You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter截取Widget生成PNG时如何确认RenderRepaintBoundary已就绪

问题根因

你遇到的报错不是因为RenderRepaintBoundary为空,而是它虽然完成了实例化,但还没走完布局、绘制的完整渲染流程,内部的绘制层还未初始化,调用toImage时触发了Flutter源码内部的空校验异常。你之前只校验了boundary本身非空,没有校验它是否已经处于可截图的就绪状态。

修复方案
  • 新增两个就绪状态判断:
    1. boundary.attached:确认渲染对象已经挂载到渲染树
    2. boundary.hasSize:确认布局阶段完成,渲染对象已经有确定的尺寸
  • 移除硬编码的固定延时:固定延时在不同性能的设备上可靠性极低,改用递归重试加重试次数上限,避免无限递归
  • 增加异常捕获兜底:避免极端场景下的崩溃
修改后的核心代码
typedef void ImageCreatedCallback(UI.Image image);

class POIWidget extends StatefulWidget {
  const POIWidget({Key key, this.poi, this.imageCreatedCallback})
      : super(key: key);
  final POI poi;
  final ImageCreatedCallback imageCreatedCallback;

  @override
  _POIWidgetState createState() => _POIWidgetState();
}

class _POIWidgetState extends State<POIWidget> {
  GlobalKey globalKey = GlobalKey();
  // 增加重试次数上限,避免无限递归
  static const int _maxRetryCount = 10;
  int _currentRetryCount = 0;

  Future<void> _capturePng() async {
    try {
      final renderObject = globalKey.currentContext?.findRenderObject();
      if (renderObject == null || renderObject is! RenderRepaintBoundary) {
        return _retryCapture();
      }
      final boundary = renderObject;
      // 新增就绪状态判断
      if (!boundary.attached || !boundary.hasSize || boundary.size.isEmpty) {
        return _retryCapture();
      }
      final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
      final image = await boundary.toImage(pixelRatio: devicePixelRatio);
      widget.imageCreatedCallback(image);
    } catch (e) {
      // 异常兜底重试
      return _retryCapture();
    }
  }

  Future<void> _retryCapture() async {
    if (_currentRetryCount >= _maxRetryCount) {
      // 超出重试次数可根据业务需要做降级处理
      debugPrint('POIWidget截图超出最大重试次数,生成失败');
      return;
    }
    _currentRetryCount++;
    await Future.delayed(const Duration(milliseconds: 16)); // 间隔一帧的时间重试
    return _capturePng();
  }

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) => _capturePng());
  }

  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      key: globalKey,
      child: ClipOval(
        child: Material(
          color: widget.poi.iconBackgroundColor,
          child: SizedBox(
            width: widget.poi.iconSize.size * 2,
            height: widget.poi.iconSize.size * 2,
            child: widget.poi.iconName != null
                ? IconManager.instance.getIconFromName(
                    widget.poi.iconName,
                    size: widget.poi.iconSize.size,
                    color: widget.poi.iconColor,
                  )
                : IconManager.instance.iconMapPin,
          ),
        ),
      ),
    );
  }
}
可选优化

如果这个POIWidget仅用于生成MapBox所需的图标,不需要显示到页面上,可以用Offstage组件包裹,隐藏组件的同时不影响截图生成:

Offstage(
  child: POIWidget(
    poi: yourPoi,
    imageCreatedCallback: (image) {
      // 处理生成的图标
    },
  ),
)

内容的提问来源于stack exchange,提问作者Lee Probert

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.16 12:24:10