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即会被绘制到屏幕上。