Erlernen Sie schnell die Kernkompetenzen der Flutter-Bildentwicklung

Hallo zusammen, ich bin 17.

Die Verwendung von Bildern in Flutter ist eine der grundlegendsten Funktionen. Als erster Artikel nach dem Start des Frühlingsfestes hat 17 sorgfältige Vorbereitungen getroffen, und es ist voll mit Trockenware! Dieser Artikel stellt die Verwendung von Bildern in Flutter so detailliert wie möglich mit vollständigen Beispielen vor und wird enthalten sein!

Verwenden Sie Webbilder

Die Verwendung von Netzwerkbildern ist sehr einfach, geben Sie einfach die Netzwerkadresse direkt ein.Nachdem Sie dieses Beispiel ausgeführt haben, wird ein Bild einer Eule angezeigt.

Der vollständige Code kann durch Einfügen in main.dart verwendet werden. Der folgende Code gibt nur bildbezogen an.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  final imageSrc =
      'https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/04ec6088c3c544a2b9459582e335483c~tplv-k3u1fbpfcp-watermark.image?';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
          body: Center(child: Image.network(imageSrc)),
    ));
  }
}

复制代码

Nachdem das Bild erfolgreich geladen wurde, wird es ungeachtet des HTTP-Anforderungsheaders zwischengespeichert, und das Bild wird direkt aus dem Speicher gelesen, wenn das Bild das nächste Mal angefordert wird.

Im Allgemeinen müssen wir die Breite und Höhe des Bildes angeben, damit es in der angegebenen Größe angezeigt werden kann. Vermeiden Sie Bilder, die zu groß sind, um das Layout zu stören.

Image.network(imageSrc,width: 100,height: 100,)
复制代码

Wenn cacheWidth oder cacheHeight bereitgestellt wird, zeigt dies der Engine an, dass das Bild mit der angegebenen Größe decodiert werden soll. Unabhängig von diesen Parametern wird das Bild gemäß den Einschränkungen gerendert. cacheWidth und cacheHeight dienen hauptsächlich dazu, die Speichernutzung von ImageCache zu reduzieren.

cacheWidth und cacheHeight werden verwendet, um den Speicher zu optimieren. Wenn Sie sicherstellen können, dass die Größe des Netzwerkabbilds angemessen ist, müssen Sie diese beiden Parameter nicht festlegen. Wenn die Größe des Quellbildes nicht garantiert werden kann, z. B. wenn es sich um ein großformatiges Bild handelt, ist es am besten, diese beiden Parameter einzustellen. Diese beiden Parameter können nur die Speichernutzung optimieren, nicht hilfreich für das Herunterladen und Decodieren. Wenn Sie den Download optimieren möchten, müssen Sie das Bild auf der Festplatte zwischenspeichern und es beim nächsten Mal direkt von der Festplatte lesen, genau wie der Webcache.

Zwischenspeichern von Webbildern auf der Festplatte

Wir können das Cached_network_image-Plugin verwenden, um Netzwerkbilder auf der Festplatte zwischenzuspeichern.

Plugin installieren

flutter pub add cached_network_image
复制代码

Der einzige erforderliche Parameter ist imageUrl.

 MaterialApp(
    home: Scaffold(
      body: Center(child: CachedNetworkImage(
        imageUrl: imageSrc,
    )),
 ));
复制代码

cached_network_image kommt mit dem Effekt von fadeIn, Placeholder wird beim Laden des Bildes angezeigt und errorWidget wird angezeigt, wenn ein Fehler auftritt.

CachedNetworkImage(
        imageUrl: imageSrc,
        placeholder: (context, url) => CircularProgressIndicator(),
        errorWidget: (context, url, error) => Icon(Icons.error),
 )
复制代码

Manchmal müssen wir Bilder auf andere Widgets anwenden, z. B. in BoxDecoration, und wir müssen imageProvider bereitstellen.


CachedNetworkImage(
  imageUrl: imageSrc,
  imageBuilder: (context, imageProvider) => Container(
    decoration: BoxDecoration(
      image: DecorationImage(
          image: imageProvider,
          fit: BoxFit.cover,
          colorFilter:
              ColorFilter.mode(Colors.red, BlendMode.colorBurn)),
    ),
  ),
  placeholder: (context, url) => CircularProgressIndicator(),
  errorWidget: (context, url, error) => Icon(Icons.error),
),
复制代码

Es gibt viele weitere Parameter, die in der

Asset-Bild verwenden

assets 也可以叫做资源。资源是与您的应用程序一起捆绑和部署的文件,可在运行时访问。常见的资源类型包括静态数据(例如 JSON 文件)、配置文件、图标和图像(JPEG、WebP、GIF、动画 WebP/GIF、PNG、BMP 和 WBMP)。

每个资源都由资源文件所在的显式路径(相对于 pubspec.yaml 文件)标识。声明资源的顺序无关紧要。包含资源的目录名称无关紧要。

在构建过程中,Flutter 将资源放入一个名为资源包的特殊存档中,应用程序会在运行时从中读取。

Flutter 使用位于项目根目录的 pubspec.yaml 文件来识别应用程序所需的资源。

资源文件夹的名称是随意的,我们可以把资源文件夹放在和 lib 平级的根目录下面,为图片建立文件夹 images,把上面示例中的猫头鹰图片放入其中。

image.png

修改 pubspec.yaml 的配置。

flutter:
  assets:
    - images/owl.png
复制代码

注意空格

在代码中可以通过 images/owl.png 使用图片。

Image.asset(
    'images/owl.png',
    width: 200,
    height: 200,
);
复制代码

运行,成功显示了猫头鹰的图片。当你发布应用程序的时候,pubspec.yaml 中配置的图片会和代码一起打包发布。

如果有很多图片,这样一张一张注册很是麻烦,我们可以直接指定文件夹。比如我们可以一次性注册 images 文件夹下面的所有图片。

flutter:
  assets:
    - images/
复制代码

适配浅色与深色模式

正常情况下,我们用的是浅色模式,在弱光环境下,打开深色模式可获得出色的视觉体验。

构建过程支持资源变体的概念:可以在不同上下文中显示的资源的不同版本。当在 pubspec.yaml 的资源部分指定资源的路径时,构建过程会在相邻子目录中查找任何具有相同名称的文件。然后,此类文件与指定资源一起包含在资源包中。

在 images 下面增加 dark 文件夹,增加在深色模式下使用的与浅色文件同名的图片。17 的电脑屏幕截图:

image.png

适配浅色与深色模式的工作就完成了!

images/owl.png 和 images/dark/owl.png 都包含在您的资源包中。前者被视为主要资源,而后者被视为变体。在浅色模式下,Flutter 为我们显示显示 images/owl.png ,在深色模式下显示 images/dark/owl.png。

在不同的设备使用不同分辨率的图片

Flutter 可以根据当前设备像素比加载分辨率合适的图像。

在 image 文件夹下增加 2.0x,3.0x文件夹,放入同名的高分辨率的图片。17 的电脑屏幕的截图:

image.png

1.5x 文件夹也是合法的。 适合不同设备分辨率的工作就完成了!

images/2.0x/owl.png 和 images/3.0x/owl.png 都包含在您的资源包中,都被视为变体。 flutter 会自动为我们在 dpr 为 2 的设备上使用 images/2.0x/owl.png 在 dpr 为 3 的设备上使用 images/3.0x/owl.png,在 dpr 为 1 设备上使用 images/owl.png。 images/owl.png 相当于是 images/1.0x/owl.png。

dpr 为设备分辨率(device pixel ratio)英文单词的首字母

还是一样的代码,现在可以适配不同 dpr 的设备!

Image.asset(
    'images/owl.png',
    width: 200,
    height: 200,
);
复制代码

关于设备 dpr 不完全匹配的处理

Flutter 以 dpr 2.0 为界,采用不同的处理方案,目的是为了得到更好的体验。

  1. 2.0 以下的设备匹配分辨率更高的图片
  2. 2.0 以上的设备匹配分辨率最接近的图片

比如有一个 dpr 为 1.25 的设备,会采用 2.0 的图片,而不是 1.0的图片。再比如有一个 dpr 为 2.25 的设备,会采用 2.0 的图片,而不会采用 3.0 的图片。

忽略 dpr 信息

如果要忽略 dpr 信息直接读取主资源(就是不带 x.0路径的那个),用 ExactAssetImage。可以指定 scale,默认为 1.0。

如果 scale 为 2.0,则意味着每个逻辑像素对应四个图像像素。看下实际的效果就明白了。

image.png

Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Image(image: ExactAssetImage('images/owl.png', scale: 5)),
          Image(image: ExactAssetImage('images/owl.png', scale: 10)),
        ],
      )
复制代码

scale 越大,图片显示的越小,因为 scale 越大,每个逻辑像素对应的图像像素就越多。

逻辑像素,也叫 设备独立像素(device independent pixels),简称 dip ,与具体设备无关。

使用相册图片

先安装插件

flutter pub add image_picker
复制代码

使用相册图片需要两步

  1. 使用 image_picker 插件从相册中读取图片
  2. 使用 Image.file 展示图片
class MyWidget extends StatefulWidget {
  const MyWidget({super.key});

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final _picker = ImagePicker();
  File? _file;
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        ElevatedButton(
            onPressed: () async {
              var xfile = await _picker.pickImage(
                  source: ImageSource.gallery,
                  maxWidth: 200,
                  maxHeight: 300,
                  requestFullMetadata: false);

              if (xfile != null) {
                setState(() {
                  _file = File(xfile.path);
                });
              }
            },
            child: const Text('从相册中选择图片')),
        if (_file != null) Image.file(_file!)
      ],
    );
  }
}

复制代码

maxWidthmaxHeight 最好设置一下,从源头上控制一下图片的大小,提高效率。如果这里没控制大小,就必须让 Image.file 加上 cacheWidthcacheHeight参数,因为用 Image.file 显示的图片也会缓存起来,需要控制缓存的图片大小,减少内存消耗。

requestFullMetadata: false 是为了避免 ios 闪退。我们在 info.plist( ios / Runner 下面 ) 中申请权限才可以请求完整的 metadata。

打开 info.plist 的 dict 中加入如下内容就可以了。

<key>NSPhotoLibraryUsageDescription</key>
<string>APP需要您的同意,才能使用相册,以便于上传,发布照片</string>
复制代码

android 不需要申请权限。

使用相机拍摄的图片

和使用相册图片步骤一样,有两点不同

  1. source: ImageSource.gallery 修改为 source: ImageSource.camera
  2. 申请相机的权限

android 不需要申请权限,直接可以使用相机,ios 需要 打开 info.plist 在 dict 中加入如下内容

<key>NSCameraUsageDescription</key>
<string>APP需要您的同意,才能使用摄像头,以便于相机拍摄,上传、发布照片</string>
复制代码

使用内存图片

Image.memory 的必选参数 bytes 是 Uint8List 类型,base64Decode 的返回值正好是 Uint8List,我们用 Image.memory 展示一下 base64 格式的图片。

我们得到的 base64格式的图片可能是这样的

imageString = 'image/jpeg;base64,/9j/4AA...'
复制代码

image/jpeg;base64, 删除,只保留后面的数据,这样才能正常显示。

引用 dart:convert 把 imageString 用 base64Decode 转成 Uint8List 类型,Image.memory 就能显示了。

import 'dart:convert';
Image.memory(base64Decode(imageString));
复制代码

图片用做装饰

DecoratedBox 是专门用来做装饰的 widget

DecoratedBox(
     decoration: BoxDecoration(
       image: DecorationImage(image: AssetImage('images/owl.png')),
     ),
     child: SizedBox(
       width: 100,
       height: 100,
     ),
   )
复制代码

更多时候,我们可以用 Container。

Container(
     width: 100,
     height: 100,
     decoration: BoxDecoration(
        image: DecorationImage(image: AssetImage('images/owl.png'))
     ),
   )
复制代码

DecorationImage 的 image 参数类型是 ImageProvider,ImageProvider 的子类都可以用作参数。除了 AssetImage,还可以用 FileImage,MemoryImage,NetworkImage。

图片预加载

在网页中的轮播图中我们一般都会做图片的预加载,用 js 预加载图片,避免图片在轮播时无法显示。Flutter 中也有轮播图,我们也可以做类似的事情。

和 js 预加载一样,Flutter 预加载图片也是很简单的。

preload(BuildContext context) {
    var configuration = createLocalImageConfiguration(context);
    for (var src in ['图片地址1', '图片地址2', '图片地址13']) {
      NetworkImage(src).resolve(configuration);
    }
  }
复制代码

图片什么时候加载完成不用管。Flutter 会用 NetWorkImage 做 key,缓存图片,下次用 NetworkImage 加载同样的图片,无论是否加载完成,都不会再次加载。

resolve 的作用就是把加载的工作提前执行。

判断两个 NetWorkImage 相同,需要 url,scale 都相同,所以如果如果 scale 不同,会触发重新加载。

  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is NetworkImage
        && other.url == url
        && other.scale == scale;
  }
复制代码

centerSlice

centerSlice 用来切 9图的。比如下面这张图片,我们用 9 图的方式来切图。

btn.png

这张图是300 x 300 的图,把它显示成 400 x 400。我们把它分成 9 个区域。

  • 区域 5 会被垂直水平拉伸
  • 区域 4,6 被垂直拉伸
  • 区域 2,8 被水平拉伸
  • 1,3,7,9 保持原样
Column(
   mainAxisSize: MainAxisSize.min,
   children: [
     Image.asset("images/btn.png",width: 300,height: 300,fit: BoxFit.fill,),
     Image.asset("images/btn.png",
     centerSlice: Rect.fromLTRB(100, 100, 200,200),width: 400,height:400,scale: 1,)
  ],
);
复制代码

根据 9 图的特性,我们可以把按钮的背景图做 9 图,用来容纳可变化的字数,也可以把聊天用的气泡做成 9 图。

centerSlice 只能放大,不能缩小。

原图是 300 x 300, centerSlice 处理后的图只能是宽比 300 大,高也比300 大,否则报错。

全局缓存 ImageCache 的置

ImageCache 有两个属性

  1. maximumSize 可以获取和设置可以缓存的图片的最大数量,默认 1000 张。
  2. maximumSizeBytes 可以获取和设置可以缓存的图片的最大容量,默认 100M。

既然这两个属性可以让我们设置,就说明在有的时候,这两个属性的默认值是不合适的。我们可以通过 PaintingBinding.instance.imageCache 拿到全局 ImageCache 的实例,通过PaintingBinding.instance.imageCache.currentSize 监控一下当前已经缓存的图片数量,如果经常达到最大值,说明默认值太小,可以设置的更大些。

图片类之间的关系

如果只是想会用 Flutter Image,上面的内容就够用了,可以跳过后面的内容。

上面讲了很多,都是零散的,涉及到的类很多,难免有混乱之感,所以需要对他们之间的关系梳理一下。

Image widget 是直接给我们用的。抛开那些命名构造函数,如果我们直接用 Image,只有一个必须的参数 image,image 类型是 ImageProvider。

ImageProvider

abstract class ImageProvider<T> {

  ImageStream resolve(ImageConfiguration configuration) {
    // 省略
  }
  Future<bool> evict({ ImageCache cache,
       ImageConfiguration configuration = ImageConfiguration.empty }) async {
    // 省略
  }

  Future<T> obtainKey(ImageConfiguration configuration); 
  @protected
  ImageStreamCompleter load(T key); // 需子类实现
}

复制代码

obtainKey(ImageConfiguration) 方法

该接口主要是为了配合实现图片缓存,不同的 key 代表不同的图片数据缓存。ImageProvider 从数据源加载完数据后,会在全局的 ImageCache 中缓存图片。

resolve(ImageConfiguration) 方法

ImageStream resolve(ImageConfiguration configuration) {
  ... //省略
  final ImageStream stream = ImageStream();
  T obtainedKey; //
  //定义错误处理函数
  Future<void> handleError(dynamic exception, StackTrace stack) async {
    ... //省略
    stream.setCompleter(imageCompleter);
    imageCompleter.setError(...);
  }

  // 创建一个新Zone,为了当发生错误时不会干扰 MainZone
  final Zone dangerZone = Zone.current.fork(...);
  
  dangerZone.runGuarded(() {
    Future<T> key;
    // 先验证是否已经有缓存
    try {
      // 生成缓存key,后面会根据此key来检测是否有缓存
      key = obtainKey(configuration);
    } catch (error, stackTrace) {
      handleError(error, stackTrace);
      return;
    }
    key.then<void>((T key) {
      obtainedKey = key;
      // 缓存的处理逻辑
      final ImageStreamCompleter completer = PaintingBinding.instance
          .imageCache.putIfAbsent(key, () => load(key), onError: handleError);
      if (completer != null) {
        stream.setCompleter(completer);
      }
    }).catchError(handleError);
  });
  return stream;
}
复制代码

resolve 是 ImageProvider 对外暴露的主要方法,我们可以调用这个方法来加载图片,Image Widget 也是调用这个方法加载图片。

要从 ImageProvider 获取 ImageStream,调用 resolve 并向其传递一个 ImageConfiguration 对象。 ImageProvider 通过 obtainKey 获得 Key 并 使用全局的 imageCache 缓存图片。

类型参数 T 是用于表示已解析配置的对象的类型。这也是图像缓存中用于键的类型。它应该是不可变的并实现 == 运算符和 hashCode getter。

AssetBundleImageProvider,FileImage,MemoryImage,NetworkImage

这四个都是 ImageProvider 的子类,AssetBundleImageProvider 又有两个子类,AssetImage 和 ExactAssetImage,这两个和 FileImage,MemoryImage,NetworkImage 都可以直接给 image 参数赋值。

比如我们要读取 owl.png

 Image(image: AssetImage("image/owl.png"),);
 Image.asset("image/owl.png");
复制代码

这两种都能显示 owl.png。那么有什么区别呢?看我们看下 Image.asset 构造函数的源码

Image.asset(
    String name, {
    省略...
  }) : image = ResizeImage.resizeIfNeeded(
         cacheWidth,
         cacheHeight,
         scale != null
           ? ExactAssetImage(name, bundle: bundle, scale: scale, package: package)
           : AssetImage(name, bundle: bundle, package: package),
       ),
       省略...
      ;
复制代码

Image.asset 构造函数 为我们创建了 ImageProvider。

  1. 如果 scale 不为空,创建 ExactAssetImage,否则创建 AssetImage
  2. 用 ResizeImage.resizeIfNeeded 包起来。

ResizeImage.resizeIfNeeded 执行下面的逻辑:如果 cacheWidth,cacheHeight 同时为空,直接返回原 ImageProvider,否则返回 ResizeImage。

static ImageProvider<Object> resizeIfNeeded(int? cacheWidth, int? cacheHeight, ImageProvider<Object> provider) {
    if (cacheWidth != null || cacheHeight != null) {
      return ResizeImage(provider, width: cacheWidth, height: cacheHeight);
    }
    return provider;
}
复制代码

ResizeImage 在放进缓存之前,会根据 cacheWidth,cacheHeight 对图片做优化,这对于减少内存开销有帮助。

从以上可以看出,没有特殊需要,我们都直接使用 Image.asset、Image.network、Image.file、age.memory,这四个命名构造函数。

ImageProvider 的子类还有一个 ScrollAwareImageProvider, RawImage 会调用他避免在快速滚动时加载图像。我们一般不需要直接使用他。

本文到这里就结束了,谢谢观看!

おすすめ

転載: juejin.im/post/7194239516709191740