当前位置:   article > 正文

Flutter截屏与长截屏的实现

Flutter截屏与长截屏的实现

在做App开发中,获取当前视图的截图基本都会用到的,在Android中,我们可以通过视图的id获取当前视图的bitmap进行编辑操作,在Flutter中想获取Widget的截图针对不同的场景也是需要一个key进行绑定截图。

这里介绍的Flutter截图的方式主要分为两种:视图可见时的截图与不可见的截图。

一、可见视图截图

需要用到的组件主要有

1)GlobalKey

2)RenderRepaintBoundary

1,创建一个GlobalKey对象
GlobalKey paintKey = new GlobalKey();
2,使用RepaintBoundary包裹需要截图的Widget,并把创建的GlobalKey与之绑定
  1. RepaintBoundary(
  2. key: paintKey,//需要注意,此处绑定创建好的GlobalKey对象
  3. child: Column(//需要截图的Widget
  4. mainAxisAlignment: MainAxisAlignment.center,
  5. children: <Widget>[
  6. const Text(
  7. 'You have pushed the button this many times:',
  8. ),
  9. Text(
  10. '$_counter',
  11. style: Theme.of(context).textTheme.headlineMedium,
  12. ),
  13. ],
  14. ),
  15. )
3,根据GlobalKey对象进行截图编译或保存本地
  1. //compressionRatio:截图的图片质量,默认值是:1.0
  2. static Future<String?> capturePng(GlobalKey paintKey,double compressionRatio) async {
  3. try {
  4. RenderRepaintBoundary? boundary =
  5. paintKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
  6. var image = await boundary?.toImage(pixelRatio: compressionRatio);
  7. ByteData? byteData = await image?.toByteData(format: ImageByteFormat.png);
  8. //getApplicationSupportDirectory需要引用path_provider库
  9. final directory = await getApplicationSupportDirectory();
  10. //这里需要导入import 'dart:io';很多人第一次导包会默认导入import 'dart:html';导致报错
  11. var imgFile = await File(
  12. '${directory.path}/${DateTime.now().millisecondsSinceEpoch}.png')
  13. .create();
  14. Uint8List? pngBytes = byteData?.buffer.asUint8List();
  15. //把Widget当前帧数据写入File文件中
  16. await imgFile.writeAsBytes(pngBytes!);
  17. return imgFile.path;
  18. } catch (e) {
  19. print(e);
  20. }
  21. return null;
  22. }

核心就是根据之前创建的GlobalKey对象,使用RenderRepaintBoundary获取Widget渲染完成的当前帧内容保存成文件格式进行二次编辑操作,主要注意的点就是File的导包,针对不熟悉Flutter的人几乎都会遇到的一个错误,至此获取Widget截图的方式已经实现,但这只针对一个简单的截图方式。

如果要针对滑动的列表进行截图,则需要使用SingleChildScrollView包裹一层,不然无法截全,例如:

  1. SingleChildScrollView(
  2. child: RepaintBoundary(
  3. key: paintKey,
  4. child: Column(
  5. mainAxisAlignment: MainAxisAlignment.center,
  6. children: <Widget>[
  7. const Text(
  8. 'You have pushed the button this many times:',
  9. ),
  10. Text(
  11. '$_counter',
  12. style: Theme.of(context).textTheme.headlineMedium,
  13. ),
  14. ],
  15. ),
  16. ),
  17. )

截取长图时,如果列表数据很长超过上千条数据时,截出来的图片就会变的很模糊,针对这种场景,建议让后端直接生成图片或者pdf,App侧直接使用或预览,毕竟App本质工作就是一个视图预览的。

二、不可见的视图截屏

开发过程中不免有些场景是不需要预览直接截图保存本地的,如果增加预览操作会影响用户的使用体验,这是就需要用到不可见的视图截屏方式。

  1. Future<Uint8List> captureInvisibleWidget(
  2. Widget widget, {
  3. Duration delay = const Duration(seconds: 1),
  4. double? pixelRatio,
  5. BuildContext? context,
  6. Size? targetSize,
  7. }) async {
  8. ui.Image image = await widgetToUiImage(widget,
  9. delay: delay,
  10. pixelRatio: pixelRatio,
  11. context: context,
  12. targetSize: targetSize);
  13. final ByteData? byteData =
  14. await image.toByteData(format: ui.ImageByteFormat.png);
  15. image.dispose();
  16. return byteData!.buffer.asUint8List();
  17. }
  18. /// If you are building a desktop/web application that supports multiple view. Consider passing the [context] so that flutter know which view to capture.
  19. static Future<ui.Image> widgetToUiImage(
  20. Widget widget, {
  21. Duration delay = const Duration(seconds: 1),
  22. double? pixelRatio,
  23. BuildContext? context,
  24. Size? targetSize,
  25. }) async {
  26. ///
  27. ///Retry counter
  28. ///
  29. int retryCounter = 3;
  30. bool isDirty = false;
  31. Widget child = widget;
  32. if (context != null) {
  33. ///
  34. ///Inherit Theme and MediaQuery of app
  35. ///
  36. ///
  37. child = InheritedTheme.captureAll(
  38. context,
  39. MediaQuery(
  40. data: MediaQuery.of(context),
  41. child: Material(
  42. child: child,
  43. color: Colors.transparent,
  44. )),
  45. );
  46. }
  47. final RenderRepaintBoundary repaintBoundary = RenderRepaintBoundary();
  48. final platformDispatcher = WidgetsBinding.instance.platformDispatcher;
  49. final fallBackView = platformDispatcher.views.first;
  50. final view = fallBackView;
  51. Size logicalSize =
  52. targetSize ?? view.physicalSize / view.devicePixelRatio; // Adapted
  53. Size imageSize = targetSize ?? view.physicalSize; // Adapted
  54. assert(logicalSize.aspectRatio.toStringAsPrecision(5) ==
  55. imageSize.aspectRatio
  56. .toStringAsPrecision(5)); // Adapted (toPrecision was not available)
  57. final RenderView renderView = RenderView(
  58. window: view,
  59. child: RenderPositionedBox(
  60. alignment: Alignment.center, child: repaintBoundary),
  61. configuration: ViewConfiguration(
  62. size: logicalSize,
  63. devicePixelRatio: pixelRatio ?? 1.0,
  64. ),
  65. );
  66. final PipelineOwner pipelineOwner = PipelineOwner();
  67. final BuildOwner buildOwner = BuildOwner(
  68. focusManager: FocusManager(),
  69. onBuildScheduled: () {
  70. ///
  71. ///current render is dirty, mark it.
  72. ///
  73. isDirty = true;
  74. });
  75. pipelineOwner.rootNode = renderView;
  76. renderView.prepareInitialFrame();
  77. final RenderObjectToWidgetElement<RenderBox> rootElement =
  78. RenderObjectToWidgetAdapter<RenderBox>(
  79. container: repaintBoundary,
  80. child: Directionality(
  81. textDirection: TextDirection.ltr,
  82. child: child,
  83. )).attachToRenderTree(
  84. buildOwner,
  85. );
  86. ///Render Widget
  87. ///
  88. ///
  89. buildOwner.buildScope(
  90. rootElement,
  91. );
  92. buildOwner.finalizeTree();
  93. pipelineOwner.flushLayout();
  94. pipelineOwner.flushCompositingBits();
  95. pipelineOwner.flushPaint();
  96. ui.Image? image;
  97. do {
  98. ///
  99. ///Reset the dirty flag
  100. ///
  101. ///
  102. isDirty = false;
  103. image = await repaintBoundary.toImage(
  104. pixelRatio: pixelRatio ?? (imageSize.width / logicalSize.width));
  105. ///
  106. ///This delay sholud increas with Widget tree Size
  107. ///
  108. await Future.delayed(delay);
  109. ///
  110. ///Check does this require rebuild
  111. ///
  112. ///
  113. if (isDirty) {
  114. ///
  115. ///Previous capture has been updated, re-render again.
  116. ///
  117. ///
  118. buildOwner.buildScope(
  119. rootElement,
  120. );
  121. buildOwner.finalizeTree();
  122. pipelineOwner.flushLayout();
  123. pipelineOwner.flushCompositingBits();
  124. pipelineOwner.flushPaint();
  125. }
  126. retryCounter--;
  127. ///
  128. ///retry untill capture is successfull
  129. ///
  130. } while (isDirty && retryCounter >= 0);
  131. try {
  132. /// Dispose All widgets
  133. // rootElement.visitChildren((Element element) {
  134. // rootElement.deactivateChild(element);
  135. // });
  136. buildOwner.finalizeTree();
  137. } catch (e) {}
  138. return image; // Adapted to directly return the image and not the Uint8List
  139. }

使用时,直接调用captureInvisibleWidget 传入所需要的截图和截图质量即可获取到视图的照片数据,用于其他用途。

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号