Flutter Widget 截图(一)

6/10/2022 flutter

Flutter Widget 截屏,将图片保存在本地并且实现图片分享的功能。

# 实战

首先让我们从一个 Widget 开始,一个红色的容器,里面有一些文本。下面是它的代码:

Container(
    padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 30),
    decoration: BoxDecoration(
        color: Colors.red,
        borderRadius: BorderRadius.circular(20),
        boxShadow: const [
            BoxShadow(color: Colors.black12, blurRadius: 10),
        ]),
    clipBehavior: Clip.antiAlias,
    child: Column(
        mainAxisSize: MainAxisSize.min,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
        const Text(
            "OldBirds",
            style: TextStyle(
            fontWeight: FontWeight.bold,
            fontSize: 25,
            ),
        ),
        const SizedBox(
            height: 20,
        ),
        Row(
            mainAxisSize: MainAxisSize.min,
            children: const [
            Icon(Icons.accessibility),
            Text('Hello World'),
            ],
        )
        ],
    ),
)

这是上面的 Widget 在视觉上的样子

# 用 RepaintBoundary 包裹它

要将此 Widget 转换为图像文件(或捕获/截屏),您需要将小部件包装在RepaintBoundary 中,然后需要创建一个 GlobalKey 并将其传递给 RepaintBoundary。

GlobalKey repaintWidgetKey = GlobalKey(); // 绘图key值

...

RepaintBoundary(
    key: repaintWidgetKey,
    child: Container(
    padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 30),
    decoration: BoxDecoration(
        color: Colors.red,
        borderRadius: BorderRadius.circular(20),
        boxShadow: const [
            BoxShadow(color: Colors.black12, blurRadius: 10),
        ]),
    clipBehavior: Clip.antiAlias,
    child: Column(
        mainAxisSize: MainAxisSize.min,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
        const Text(
            "OldBirds",
            style: TextStyle(
            fontWeight: FontWeight.bold,
            fontSize: 25,
            ),
        ),
        const SizedBox(
            height: 20,
        ),
        Row(
            mainAxisSize: MainAxisSize.min,
            children: const [
            Icon(Icons.accessibility),
            Text('Hello World'),
            ],
        )
        ],
    ),
    ),
)

# 创建图像

获取图像字节的代码。

final boundary = key.currentContext?.findRenderObject() as RenderRepaintBoundary?;
final image = await boundary?.toImage();
final byteData = await image?.toByteData(format: ImageByteFormat.png);
final imageBytes = byteData?.buffer.asUint8List();

# 保存本地

将这些字节写入某处的文件,需要 path_provider 使用该包getTemporaryDirectory:

if (imageBytes != null) {
    Directory tempDir = await getTemporaryDirectory();
    String storagePath = tempDir.path;
    File file = File(
        '$storagePath/oldbird_gen_image_${DateTime.now().millisecondsSinceEpoch}.png');
    debugPrint('::file:path: ${file.path}');
    if (!file.existsSync()) {
      file.createSync();
    }
    file.writeAsBytesSync(sourceBytes);
}

# 分享图片

将保存到本地的图片,通过 share_plus 实现分享图片的功能:

await Share.shareFiles([file.path], text: 'Great picture');

# 说明

# RepaintBoundary

Flutter 提供了支持截屏的 RepaintBoundary,在需要截取部分的外层嵌套,也可以截取某一子 Widget 内容;RepaintBoundary 的结构很简单,通过 key 来判断截取的 RenderObject,最终生成一个 RenderRepaintBoundary 对象;

RenderRepaintBoundary 是一个对象缓存着 widget 的 UI data 信息,当 viewTree 要重绘的时候,可以提高绘制效率。

# ui.Image

通过 RenderRepaintBoundary 获取的对象 .toImage() 的方法,对 widget 进行截屏,并输出其当前的 UI data,返回的 ui.Image 是原始的 RGBA bytes,pixelRatio 参数可以设置要输出图片的分辨率,默认一倍图,可根据情况调整。

toByteData() 生成的数据格式一般分三种:

  • rawRgba:未解码的 byte;
  • rawUnmodified:未解码且未修改的 byte,如灰度图;
  • png 为我们常用的 PNG 图片;

# Directory

若需要存储本地,跟 Android/iOS 类似,首先获取存储路径,再进行存储操作;小菜借助三方插件 path_provider 来获取图片路径;

path_provider 提供了 getTemporaryDirectory(临时路径) / getApplicationDocumentsDirectory(全局路径)等,可以根据不同的需求存储不同路径;

# writeAsBytes

文件的保存很简单,直接将 Uint8List 写入到所在文件路径下即可;

# 完整代码

import 'dart:io';
import 'dart:typed_data';
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:ui' as ui;
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';

class CaptureImagePage extends StatefulWidget {
  const CaptureImagePage({Key? key}) : super(key: key);

  
  State<CaptureImagePage> createState() => _CaptureImagePageState();
}

class _CaptureImagePageState extends State<CaptureImagePage> {
  GlobalKey repaintWidgetKey = GlobalKey(); // 绘图key值

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.yellow,
      body: Center(
        child: RepaintBoundary(
          key: repaintWidgetKey,
          child: Container(
            padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 30),
            decoration: BoxDecoration(
                color: Colors.red,
                borderRadius: BorderRadius.circular(20),
                boxShadow: const [
                  BoxShadow(color: Colors.black12, blurRadius: 10),
                ]),
            clipBehavior: Clip.antiAlias,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Text(
                  "OldBirds",
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 25,
                  ),
                ),
                const SizedBox(
                  height: 20,
                ),
                Row(
                  mainAxisSize: MainAxisSize.min,
                  children: const [
                    Icon(Icons.accessibility),
                    Text('Hello World'),
                  ],
                )
              ],
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _shareUiImage();
        },
        tooltip: '截图',
        child: const Icon(Icons.ac_unit),
      ),
    );
  }

  /// 分享图片
  Future _shareUiImage() async {
    Uint8List? sourceBytes = await _capturePngToByteData();
    if (sourceBytes == null) {
      return;
    }
    Directory tempDir = await getTemporaryDirectory();
    String storagePath = tempDir.path;
    File file = File(
        '$storagePath/oldbird_gen_image_${DateTime.now().millisecondsSinceEpoch}.png');
    debugPrint('::file:path: ${file.path}');
    if (!file.existsSync()) {
      file.createSync();
    }
    file.writeAsBytesSync(sourceBytes);
    await Share.shareFiles([file.path], text: 'Great picture');
    ScaffoldMessenger.of(context)
        .showSnackBar(const SnackBar(content: Text("分享图片成功")));
  }

  /// 截屏图片生成图片,返回图片二进制
  Future<Uint8List?> _capturePngToByteData() async {
    try {
      RenderRepaintBoundary? boundary = repaintWidgetKey.currentContext
          ?.findRenderObject() as RenderRepaintBoundary?;
      if (boundary == null) {
        return null;
      }
      // 获取当前设备的像素比
      double dpr = ui.window.devicePixelRatio;
      ui.Image image = await boundary.toImage(pixelRatio: dpr);
      final sourceBytes = await image.toByteData(format: ImageByteFormat.png);
      return sourceBytes?.buffer.asUint8List();
    } catch (e) {
      return null;
    }
  }
}
上次更新: 6/18/2022, 10:39:15 AM