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