Flutter 进阶:NCanvasImageLoader 让 Canvas 也能画网络图

一、需求来源

开发中遇到一个痛点: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, _) { /* 加载失败 */ },
));

问题很明显:

  1. 回调式 API:不是 Futureasync 方法里没法直接 await
  2. 多图并行难:回调嵌套爆炸,没法 Future.wait
  3. 占位 / 缓存 / 错误要自己管Image widget 白送的能力,到 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;
}

两个易忽略的细节:

  • removeListenerImageStream 支持多监听者,成功/失败后必须移除,否则泄漏、重复回调;
  • 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

CustomPainterpaintImage 的参数原样暴露,照搬 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 里触发网络请求阻塞首帧;
  • mountedsetState:异步加载可能跨销毁,防止对已销毁组件 setState;
  • didUpdateWidget 里 URL 变了要重载:列表滚动复用时 URL 变化触发 _load(),其它属性只重绘;
  • devicePixelRatio: 3:按 3x 屏配置,高清屏下图不糊。

四、总结

NCanvasImageLoader 做的事很朴素:Completer 把回调式图片加载包装成 Future<ui.Image>,并内置占位兜底。配合 NImagePainter(薄封装 paintImage)和 NCanvasNetworkImage(生命周期管理),组成完整「Canvas 画网图」工具链。

核心价值:

  • 异步转同步:一句 await 拿到 ui.Image
  • 占位兜底:任何失败返回占位图,且并发预加载;
  • 能力对齐 Imagefit / 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

0
0
0
0
评论
未登录
暂无评论