Flutter截取Widget生成PNG时如何确认RenderRepaintBoundary已就绪
问题根因
你遇到的报错不是因为RenderRepaintBoundary为空,而是它虽然完成了实例化,但还没走完布局、绘制的完整渲染流程,内部的绘制层还未初始化,调用toImage时触发了Flutter源码内部的空校验异常。你之前只校验了boundary本身非空,没有校验它是否已经处于可截图的就绪状态。
修复方案
- 新增两个就绪状态判断:
boundary.attached:确认渲染对象已经挂载到渲染树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
相关产品推荐
相关产品推荐

