Flutter截屏与长截屏的实现

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

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

一、可见视图截图

需要用到的组件主要有

1)GlobalKey

2)RenderRepaintBoundary

1,创建一个GlobalKey对象
GlobalKey paintKey = new GlobalKey();
2,使用RepaintBoundary包裹需要截图的Widget,并把创建的GlobalKey与之绑定
Dart 复制代码
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,
      ),
    ],
  ),
)
3,根据GlobalKey对象进行截图编译或保存本地
Dart 复制代码
//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包裹一层,不然无法截全,例如:

Dart 复制代码
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本质工作就是一个视图预览的。

二、不可见的视图截屏

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

Dart 复制代码
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 传入所需要的截图和截图质量即可获取到视图的照片数据,用于其他用途。

相关推荐
李新_16 小时前
一文聊聊Flutter多业务混合工程实践
android·flutter
sunly_17 小时前
Flutter:flutter_screenutil屏幕适配
前端·javascript·flutter
sunly_2 天前
Flutter:AnimatedPadding动态修改padding
android·flutter
LinXunFeng2 天前
Flutter - 子部件任意位置观察滚动数据
前端·flutter·开源
leluckys2 天前
flutter 专题十二 Flutter Fair逻辑动态化架构设计与实现
flutter
sunly_2 天前
Flutter:AnimatedIcon图标动画,自定义Icon通过延时Interval,实现交错式动画
flutter
嘟嘟叽2 天前
初学 flutter 问题记录
flutter
__WanG2 天前
Flutter将应用打包发布到App Store
前端·flutter·ios
leluckys2 天前
flutter 专题十七 Flutter Flar动画实战
前端·flutter
sunly_2 天前
Flutter:AnimatedBuilder自定义显示动画
flutter