Flutter 学习:ImageProvider工作流程和AssetImage 的自动分辨率适配原理
https://cloud.tencent.com/developer/article/1748045上面流程为ImageProvider工作流程细节,作者已经写的很详细了,非常受用,现在接着上面作者内容讨论下AssetImage 加载图片数据后如何刷新。
我们知道加载图片肯定是异步的,不可能在一次刷新绘制就可以获取到图片的数据,只能是等待图片加载后再通知页面刷新,那么是如何通知页面刷新呢?
下面以一个AssetImage 加载为例进行说明。
- Center(
- child: Container(
- width: width,
- height: height,
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(radius ?? 0),
- image: DecorationImage(
- image: AssetImage(file!),
- fit: BoxFit.cover,
- )),
- ),
- )
可以看到AssetImage作为DecorationImage属性。
在Flutter RenderObject Tree中,上面的Widget最终RenderObject Tree会包RenderDecoratedBox
这个就是装饰器的RenderObject,我们直接看源码
- @override
- void paint(PaintingContext context, Offset offset) {
- assert(size.width != null);
- assert(size.height != null);
- _painter ??= _decoration.createBoxPainter(markNeedsPaint);
- ......
-
- }
可以看到在paint方法里面创建_painter 时调用了createBoxPainer方法
_painter ??= _decoration.createBoxPainter(markNeedsPaint);
这里我们记一下markNeedsPaint,后面会用到这个方法。
- @factory
- BoxPainter createBoxPainter([ VoidCallback onChanged ]);
最后实现在BoxDecoration类中
- class BoxDecoration extends Decoration {
- ......
- @override
- BoxPainter createBoxPainter([ VoidCallback? onChanged ]) {
- assert(onChanged != null || image == null);
- return _BoxDecorationPainter(this, onChanged);
- }
- ......
- }
_BoxDecorationPainter类就是具体负责绘制装饰器的类,包含绘制阴影、背景和其他各种样式。
直接看其paint方法
- @override
- void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
- assert(configuration != null);
- assert(configuration.size != null);
- final Rect rect = offset & configuration.size!;
- final TextDirection? textDirection = configuration.textDirection;
- _paintShadows(canvas, rect, textDirection);
- _paintBackgroundColor(canvas, rect, textDirection);
- _paintBackgroundImage(canvas, rect, configuration);
- _decoration.border?.paint(
- canvas,
- rect,
- shape: _decoration.shape,
- borderRadius: _decoration.borderRadius?.resolve(textDirection),
- textDirection: configuration.textDirection,
- );
- }
由于我们这里是追踪图片的渲染流程,直接看 _paintBackgroundImage(canvas, rect, configuration);这个方法。
- void _paintBackgroundImage(Canvas canvas, Rect rect, ImageConfiguration configuration) {
- if (_decoration.image == null) {
- return;
- }
- _imagePainter ??= _decoration.image!.createPainter(onChanged!);
- Path? clipPath;
- switch (_decoration.shape) {
- case BoxShape.circle:
- assert(_decoration.borderRadius == null);
- final Offset center = rect.center;
- final double radius = rect.shortestSide / 2.0;
- final Rect square = Rect.fromCircle(center: center, radius: radius);
- clipPath = Path()..addOval(square);
- break;
- case BoxShape.rectangle:
- if (_decoration.borderRadius != null) {
- clipPath = Path()..addRRect(_decoration.borderRadius!.resolve(configuration.textDirection).toRRect(rect));
- }
- break;
- }
- _imagePainter!.paint(canvas, rect, clipPath, configuration);
- }
上面主要功能:
1.创建画笔
2.在画布绘制图片
同样的流程,我们继续查看_imagePainter!.paint() 方法,这个方法里面就是本文的重点。
- /// Draw the image onto the given canvas.
- ///
- /// The image is drawn at the position and size given by the `rect` argument.
- ///
- /// The image is clipped to the given `clipPath`, if any.
- ///
- /// The `configuration` object is used to resolve the image (e.g. to pick
- /// resolution-specific assets), and to implement the
- /// [DecorationImage.matchTextDirection] feature.
- ///
- /// If the image needs to be painted again, e.g. because it is animated or
- /// because it had not yet been loaded the first time this method was called,
- /// then the `onChanged` callback passed to [DecorationImage.createPainter]
- /// will be called.
- void paint(Canvas canvas, Rect rect, Path? clipPath, ImageConfiguration configuration) {
- ......
- final ImageStream newImageStream = _details.image.resolve(configuration);
- if (newImageStream.key != _imageStream?.key) {
- final ImageStreamListener listener = ImageStreamListener(
- _handleImage,
- onError: _details.onError,
- );
- _imageStream?.removeListener(listener);
- _imageStream = newImageStream;
- _imageStream!.addListener(listener);
- }
- if (_image == null) {
- return;
- }
-
- if (clipPath != null) {
- canvas.save();
- canvas.clipPath(clipPath);
- }
-
- paintImage(
- canvas: canvas,
- rect: rect,
- image: _image!.image,
- debugImageLabel: _image!.debugLabel,
- scale: _details.scale * _image!.scale,
- colorFilter: _details.colorFilter,
- fit: _details.fit,
- alignment: _details.alignment.resolve(configuration.textDirection),
- centerSlice: _details.centerSlice,
- repeat: _details.repeat,
- flipHorizontally: flipHorizontally,
- opacity: _details.opacity,
- filterQuality: _details.filterQuality,
- invertColors: _details.invertColors,
- isAntiAlias: _details.isAntiAlias,
- );
-
- if (clipPath != null) {
- canvas.restore();
- }
- }
该方法进来先注册回调,这个回调就是等待图片加载流完成后重新回调通知刷新,如果_image==null,也就是图片未加载完成,在直接return返回,等待回调执行,
如果_image!=null 在表明图片已经加载完成,则继续流程,运行到paintImage方法
- final ImageStreamListener listener = ImageStreamListener(
- _handleImage,
- onError: _details.onError,
- );
- _imageStream?.removeListener(listener);
- _imageStream = newImageStream;
- _imageStream!.addListener(listener);
图片加载后执行_handleImage方法
- void _handleImage(ImageInfo value, bool synchronousCall) {
- if (_image == value) {
- return;
- }
- if (_image != null && _image!.isCloneOf(value)) {
- value.dispose();
- return;
- }
- _image?.dispose();
- _image = value;
- assert(_onChanged != null);
- if (!synchronousCall) {
- _onChanged();
- }
- }
直接看这个方法最后一行,调用了_onChanged()方法,它是不是很熟悉,没错这个就是markNeedsPaint方法。
总结:在渲染图片时,由于加载图片内容耗时,我们注册一个markNeedsPaint回调方法,等待图片加载后,再调用Flutter 渲染流程里面的makeNeedsPaint方法,标记该RenderObject需要绘制,那么在下一次的绘制流中,该RenderObject即会被绘制到屏幕上。