赞
踩
在做App开发中,获取当前视图的截图基本都会用到的,在Android中,我们可以通过视图的id获取当前视图的bitmap进行编辑操作,在Flutter中想获取Widget的截图针对不同的场景也是需要一个key进行绑定截图。
这里介绍的Flutter截图的方式主要分为两种:视图可见时的截图与不可见的截图。
需要用到的组件主要有
1)GlobalKey
2)RenderRepaintBoundary
GlobalKey paintKey = new GlobalKey();
- RepaintBoundary(
- key: paintKey,//需要注意,此处绑定创建好的GlobalKey对象
- child: Column(//需要截图的Widget
- mainAxisAlignment: MainAxisAlignment.center,
- children: <Widget>[
- const Text(
- 'You have pushed the button this many times:',
- ),
- Text(
- '$_counter',
- style: Theme.of(context).textTheme.headlineMedium,
- ),
- ],
- ),
- )
- //compressionRatio:截图的图片质量,默认值是:1.0
- static Future<String?> capturePng(GlobalKey paintKey,double compressionRatio) async {
- try {
- RenderRepaintBoundary? boundary =
- paintKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
- var image = await boundary?.toImage(pixelRatio: compressionRatio);
- ByteData? byteData = await image?.toByteData(format: ImageByteFormat.png);
- //getApplicationSupportDirectory需要引用path_provider库
- final directory = await getApplicationSupportDirectory();
- //这里需要导入import 'dart:io';很多人第一次导包会默认导入import 'dart:html';导致报错
- var imgFile = await File(
- '${directory.path}/${DateTime.now().millisecondsSinceEpoch}.png')
- .create();
- Uint8List? pngBytes = byteData?.buffer.asUint8List();
- //把Widget当前帧数据写入File文件中
- await imgFile.writeAsBytes(pngBytes!);
- return imgFile.path;
- } catch (e) {
- print(e);
- }
- return null;
- }

核心就是根据之前创建的GlobalKey对象,使用RenderRepaintBoundary获取Widget渲染完成的当前帧内容保存成文件格式进行二次编辑操作,主要注意的点就是File的导包,针对不熟悉Flutter的人几乎都会遇到的一个错误,至此获取Widget截图的方式已经实现,但这只针对一个简单的截图方式。
如果要针对滑动的列表进行截图,则需要使用SingleChildScrollView包裹一层,不然无法截全,例如:
- SingleChildScrollView(
- child: RepaintBoundary(
- key: paintKey,
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: <Widget>[
- const Text(
- 'You have pushed the button this many times:',
- ),
- Text(
- '$_counter',
- style: Theme.of(context).textTheme.headlineMedium,
- ),
- ],
- ),
- ),
- )

截取长图时,如果列表数据很长超过上千条数据时,截出来的图片就会变的很模糊,针对这种场景,建议让后端直接生成图片或者pdf,App侧直接使用或预览,毕竟App本质工作就是一个视图预览的。
开发过程中不免有些场景是不需要预览直接截图保存本地的,如果增加预览操作会影响用户的使用体验,这是就需要用到不可见的视图截屏方式。
- Future<Uint8List> captureInvisibleWidget(
- Widget widget, {
- Duration delay = const Duration(seconds: 1),
- double? pixelRatio,
- BuildContext? context,
- Size? targetSize,
- }) async {
- ui.Image image = await widgetToUiImage(widget,
- delay: delay,
- pixelRatio: pixelRatio,
- context: context,
- targetSize: targetSize);
- final ByteData? byteData =
- await image.toByteData(format: ui.ImageByteFormat.png);
- image.dispose();
-
- return byteData!.buffer.asUint8List();
- }
-
- /// If you are building a desktop/web application that supports multiple view. Consider passing the [context] so that flutter know which view to capture.
- static Future<ui.Image> widgetToUiImage(
- Widget widget, {
- Duration delay = const Duration(seconds: 1),
- double? pixelRatio,
- BuildContext? context,
- Size? targetSize,
- }) async {
- ///
- ///Retry counter
- ///
- int retryCounter = 3;
- bool isDirty = false;
-
- Widget child = widget;
-
- if (context != null) {
- ///
- ///Inherit Theme and MediaQuery of app
- ///
- ///
- child = InheritedTheme.captureAll(
- context,
- MediaQuery(
- data: MediaQuery.of(context),
- child: Material(
- child: child,
- color: Colors.transparent,
- )),
- );
- }
-
- final RenderRepaintBoundary repaintBoundary = RenderRepaintBoundary();
- final platformDispatcher = WidgetsBinding.instance.platformDispatcher;
- final fallBackView = platformDispatcher.views.first;
- final view = fallBackView;
- Size logicalSize =
- targetSize ?? view.physicalSize / view.devicePixelRatio; // Adapted
- Size imageSize = targetSize ?? view.physicalSize; // Adapted
-
- assert(logicalSize.aspectRatio.toStringAsPrecision(5) ==
- imageSize.aspectRatio
- .toStringAsPrecision(5)); // Adapted (toPrecision was not available)
-
- final RenderView renderView = RenderView(
- window: view,
- child: RenderPositionedBox(
- alignment: Alignment.center, child: repaintBoundary),
- configuration: ViewConfiguration(
- size: logicalSize,
- devicePixelRatio: pixelRatio ?? 1.0,
- ),
- );
-
- final PipelineOwner pipelineOwner = PipelineOwner();
- final BuildOwner buildOwner = BuildOwner(
- focusManager: FocusManager(),
- onBuildScheduled: () {
- ///
- ///current render is dirty, mark it.
- ///
- isDirty = true;
- });
-
- pipelineOwner.rootNode = renderView;
- renderView.prepareInitialFrame();
-
- final RenderObjectToWidgetElement<RenderBox> rootElement =
- RenderObjectToWidgetAdapter<RenderBox>(
- container: repaintBoundary,
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: child,
- )).attachToRenderTree(
- buildOwner,
- );
-
- ///Render Widget
- ///
- ///
-
- buildOwner.buildScope(
- rootElement,
- );
- buildOwner.finalizeTree();
-
- pipelineOwner.flushLayout();
- pipelineOwner.flushCompositingBits();
- pipelineOwner.flushPaint();
-
- ui.Image? image;
-
- do {
- ///
- ///Reset the dirty flag
- ///
- ///
- isDirty = false;
-
- image = await repaintBoundary.toImage(
- pixelRatio: pixelRatio ?? (imageSize.width / logicalSize.width));
-
- ///
- ///This delay sholud increas with Widget tree Size
- ///
-
- await Future.delayed(delay);
-
- ///
- ///Check does this require rebuild
- ///
- ///
- if (isDirty) {
- ///
- ///Previous capture has been updated, re-render again.
- ///
- ///
- buildOwner.buildScope(
- rootElement,
- );
- buildOwner.finalizeTree();
- pipelineOwner.flushLayout();
- pipelineOwner.flushCompositingBits();
- pipelineOwner.flushPaint();
- }
- retryCounter--;
-
- ///
- ///retry untill capture is successfull
- ///
- } while (isDirty && retryCounter >= 0);
- try {
- /// Dispose All widgets
- // rootElement.visitChildren((Element element) {
- // rootElement.deactivateChild(element);
- // });
- buildOwner.finalizeTree();
- } catch (e) {}
-
- return image; // Adapted to directly return the image and not the Uint8List
- }

使用时,直接调用captureInvisibleWidget 传入所需要的截图和截图质量即可获取到视图的照片数据,用于其他用途。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。