一、需求来源
开发中遇到一个痛点:CustomPainter 里只能用 ui.Image 通过 paintImage 画图,但 Flutter 的图片加载是回调式的(ImageStreamListener),不是 Future。想把网图塞进 Canvas,得自己处理异步转同步、缓存、占位、错误。
Image widget 把这些全做了,可它进不了 paint 方法。所以封装了 NCanvasImageLoader 工具链:把网图加载成 ui.Image,再丢给 paintImage 绘制。
paintImage 需要的是一个 ui.Image:
less
代码解读
复制代码
paintImage(
canvas: canvas,
rect: imgRect,
image: image!, // 需要 ui.Image,不是 URL / Widget
fit: BoxFit.contain,
);
而常规网图加载是回调式的:
scss
代码解读
复制代码
final stream = CachedNetworkImageProvider(url).resolve(configuration);
stream.addListener(ImageStreamListener(
(ImageInfo info, _) { /* 拿到 ui.Image */ },
onError: (e, _) { /* 加载失败 */ },
));
问题很明显:
- 回调式 API:不是
Future,async方法里没法直接await; - 多图并行难:回调嵌套爆炸,没法
Future.wait; - 占位 / 缓存 / 错误要自己管:
Imagewidget 白送的能力,到 Canvas 全没了。
二、使用示例
一句话把网图加载成 ui.Image,丢给 Canvas 绘制。工具链分两层,按需取用:
组件用法 —— 想要占位图、生命周期自动管理,直接用组件:
less
代码解读
复制代码
// 加载中显示占位 → 加载完用 CustomPaint 绘制
NCanvasNetworkImage(
url: url,
width: 200,
height: 100,
fit: BoxFit.cover,
);
工具用法 —— 已经在自定义 CustomPainter 里,只要 ui.Image:
php
代码解读
复制代码
final image = await NCanvasImageLoader.load(url, placeholder: myPlaceholder);
paintImage(canvas: canvas, rect: myRect, image: image, fit: BoxFit.contain);
三、源码讲解
1. 核心:Completer 把回调包成 Future
把回调转 Future 的标准工具是 Completer:拿到图 complete,出错 completeError。
ini
代码解读
复制代码
static Future<ui.Image> _loadImage(
ImageProvider provider, {
ImageConfiguration configuration = const ImageConfiguration(),
}) async {
final completer = Completer<ui.Image>();
final stream = provider.resolve(configuration);
late ImageStreamListener listener;
listener = ImageStreamListener(
(ImageInfo info, _) {
completer.complete(info.image);
stream.removeListener(listener); // 用完移除监听,防泄漏
},
onChunk: (event) {
// 可选进度:cumulativeBytesLoaded / expectedTotalBytes
},
onError: (e, _) {
completer.completeError(e);
stream.removeListener(listener);
},
);
stream.addListener(listener);
return completer.future;
}
两个易忽略的细节:
removeListener:ImageStream支持多监听者,成功/失败后必须移除,否则泄漏、重复回调;late声明 listener:回调里要引用listener自身来移除,存在循环引用,必须late。
2. 入口:网图 + 占位兜底
load() 是对外唯一入口,策略:加载失败(或非 http)返回占位图,绝不抛异常:
dart
代码解读
复制代码
static Future<ui.Image?> load(
String? url, {
required AssetImage placeholder,
ImageConfiguration configuration = const ImageConfiguration(),
}) async {
final placeholderImage = _loadImage(placeholder, configuration: configuration);
try {
if (url == null || url.startsWith("http") != true) {
return placeholderImage;
}
final provider = CachedNetworkImageProvider(url); // 复用缓存
return await _loadImage(provider, configuration: configuration);
} catch (e) {
return placeholderImage; // 任何异常都兜底
}
}
精妙点:占位图在进 try 前就启动 _loadImage,与网图并发加载,即使网图失败占位图也大概率就绪,不二次等待。
3. NImagePainter:薄封装 paintImage
CustomPainter 把 paintImage 的参数原样暴露,照搬 Image widget 能力:
arduino
代码解读
复制代码
class NImagePainter extends CustomPainter {
NImagePainter({this.image, this.fit, this.opacity = 1.0, this.colorFilter,
this.blendMode = BlendMode.srcOver, this.alignment = Alignment.center, /* ... */});
@override
void paint(Canvas canvas, Size size) {
if (image == null) return;
paintImage(
canvas: canvas,
rect: Rect.fromLTWH(0, 0, size.width, size.height),
image: image!,
fit: fit, alignment: alignment, opacity: opacity,
colorFilter: colorFilter, blendMode: blendMode, // ...
);
}
@override
bool shouldRepaint(covariant NImagePainter oldDelegate) {
return oldDelegate.image != image || oldDelegate.fit != fit /* ... */;
}
}
4. NCanvasNetworkImage:加载 + 绘制收成一个 Widget
StatefulWidget 管理生命周期:加载中显示 Image 占位,加载完切换 CustomPaint。
arduino
代码解读
复制代码
class _NCanvasNetworkImageState extends State<NCanvasNetworkImage> {
ui.Image? image;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _load()); // 首帧后加载
}
Future<void> _load() async {
image = await NCanvasImageLoader.load(
widget.url,
placeholder: widget.placeholder,
configuration: ImageConfiguration(
size: Size(widget.width, widget.height),
devicePixelRatio: 3,
),
);
if (mounted) setState(() {});
}
@override
Widget build(BuildContext context) {
if (image == null) {
return Image(image: widget.placeholder, width: widget.width, height: widget.height);
}
return CustomPaint(
size: Size(widget.width, widget.height),
painter: NImagePainter(image: image, fit: widget.fit),
);
}
}
工程细节:
addPostFrameCallback再加载:首帧先渲染占位图,避免 build 里触发网络请求阻塞首帧;mounted再setState:异步加载可能跨销毁,防止对已销毁组件 setState;didUpdateWidget里 URL 变了要重载:列表滚动复用时 URL 变化触发_load(),其它属性只重绘;devicePixelRatio: 3:按 3x 屏配置,高清屏下图不糊。
四、总结
NCanvasImageLoader 做的事很朴素:用 Completer 把回调式图片加载包装成 Future<ui.Image>,并内置占位兜底。配合 NImagePainter(薄封装 paintImage)和 NCanvasNetworkImage(生命周期管理),组成完整「Canvas 画网图」工具链。
核心价值:
- 异步转同步:一句
await拿到ui.Image; - 占位兜底:任何失败返回占位图,且并发预加载;
- 能力对齐
Image:fit/opacity/colorFilter/blendMode全透传; - 工程细节:
removeListener防泄漏、mounted防崩溃、didUpdateWidget重载、devicePixelRatio保清晰。
一句话:把 Image widget 的能力,用 Completer + paintImage 搬进 Canvas,让自定义绘制也能优雅地消费网络图。
本文源码参考:
http://www.flipsnack.com/75C5E98C5A8/2026.html http://www.flipsnack.com/75C5E98C5A8/2026-zkfraq5cjl.html http://www.flipsnack.com/75C5E98C5A8/2026-d1igfsmtk5.html http://www.flipsnack.com/75C5E98C5A8/2026-7h8os0iunf.html http://www.flipsnack.com/75C5E98C5A8/2026-c9e1w60m8y.html http://www.flipsnack.com/75C5E98C5A8/2026-xksdqyfpma.html http://www.flipsnack.com/75C5E98C5A8/2026-zksdqyfpma.html http://www.flipsnack.com/75C5E98C5A8/2026-h9e1w60m8y.html http://www.flipsnack.com/75C5E98C5A8/2026-zpa7e2ln30.html http://www.flipsnack.com/75C5E98C5A8/2026-u32h6beilw.html http://www.flipsnack.com/75C5E98C5A8/2026-7pa7e2ln30.html http://www.flipsnack.com/75C5E98C5A8/2026-hiwugoy5s4.html
