Flutter 截图上传

2024-04-21 08:41:25 浏览数 (2)

在上一篇文章中,我们谈了 Flutter 中下载并保存图片为文件 的内容,今天,我们来说说怎么将 widget 生成截图,并且怎么通过接口上传。

生成截图

我们指定生成图片的 widget 区域:

代码语言:javascript复制
final GlobalKey boundaryKey = GlobalKey();

// ... 
child: RepaintBoundary(
  key: boundaryKey,
  child: const Text('测试内容'), // 这里可以替换成自定义的 widget 内容
)

上面,我们通过 RepaintBoundary 包裹了需要生成图片的 widget。下面,我们触发方法,生成图片:

代码语言:javascript复制
Widget build(BuildContext context) {
  return IconButton(
    icon: const Icon(Icons.insert_photo_rounded),
    onPressed: () async {
      try {
          // widget 转换成图片
          RenderRepaintBoundary? boundary = boundaryKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
          ui.Image? image = await boundary?.toImage()
          
          final byteData = await image?.toByteData(format: ui.ImageByteFormat.png);
          
          // 弹窗展示
          showDialog(
            context: context,
            builder: (BuildContext context) {
              return AlertDialog(
                title: const Text(
                  '展示截图', 
                  style: TextStyle(
                    fontSize: 16.0,
                  ),
                ),
                content: SizeBox(
                  width: 520.0,
                  height: 160.0
                  child: Image.memory(
                    byteData!.buffer.asUnit8List(),
                    fit: BoxFit.fill,
                  ),
                ),
                actions: <Widget>[
                  TextButton(
                    onPressed: () {
                      Navigator.of(context).pop();
                    },
                    child: const Text(
                      '取消',
                      style: TextStyle(
                        color: Colors.grey,
                      ),
                    ),
                  ),
                ],
              );
            }
          );
      } catch(e) {
        debugPrint(e.toString());
      }
    }
  )
}

上面代码中,我们指定了 boundaryKey 区域,通过 boundary?.toImage() 生成了图片。然后,为了方便我们查看生成的图片,我们写了个弹窗,通过 Image.memory 将保存在内存的图片数据流展示出来。

保存为临时文件

接着,我们将该图片数据流写成临时的文件。

我们需要用到包 path_provider,截止发稿,该版本为 2.1.3

该插件方便我们查找文件系统的常用位置。

代码语言:javascript复制
onPressed: () async {
  // widget 转换成图片
  RenderRepaintBoundary? boundary = boundaryKey.currentContext?.findRenderObject() as   RenderRepaintBoundary?;
  ui.Image? image = await boundary?.toImage()
          
  final byteData = await image?.toByteData(format: ui.ImageByteFormat.png);
  
  // 创建一个临时的文件进行上传
  final output = await getTemporaryDirectory();
  final file = File("${output.path}/${(new DateTime.now()).millisecondsSinceEpoch}.png");
  await file.writeAsBytes(byteData!.buffer.asUint8List());
}

getTemporaryDirectory() 用于获取临时文件目录的路径。不同操作系统,获取的临时文件目录路径可能会有所不同,这个我们不需要关心。然后,我们生成了当前时间戳的文件名 DateTime.now()).millisecondsSinceEpoch}.png,并把生成的截图数据写入文件中。

接口上传

为了方便理解,我们扩展演示,随便增加一个字段,上传的 Form 表单内容如下:

代码语言:javascript复制
final TextEditingController _descriptionController = TextEditingController(text: '描述内容');

// 其他代码
Form(
  child: Column(
    children: <Widget>[
      Container(
        width: 520.0,
        height: 160.0,
        child: Image.memory(
          byteData!.buffer.asUnit8List(),
          fit: BoxFit.fill,
        ),
      ),
      TextFormField(
        controller: _descriptionController,
        decoration: InputDecoration(
          labelText: '描述'
        ),
      ),
    ],
  ),
),

我们增加了 description 图片描述的字段,所以,我们将上传如下的字段到服务器:

代码语言:javascript复制
{
  "file": *,
  "description": *,
}

通过 Bloc 管理:

代码语言:javascript复制
onPressed: () {
  fileBloc.add(
    FileSaveEvent(
      file: file,
      description: '相关描述',
    ),
  );
}
代码语言:javascript复制
// file_event.dart
abstract class FileEvent {}

class FileSaveEvent extends FileEvent {
  final File file;
  final String description;
  
  FileSaveEvent({
    required this.file,
    required this.description,
  });
}

PS 关于 bloc 的知识点,我们将会开一篇文章进行讲解,这里先一笔带过

代码语言:javascript复制
// file_bloc.dart
FileBloc() : super(FileState().init()) {
  on<FileSaveEvent>(_onFileSaveEvent);
} 

Future<String> _onFileSaveEvent(FileSaveEvent event, Emitter<FileState> emit) async {
  Map<String, dynamic> snapshot = {
    'description': event.description ?? '',
    'type': 'IMAGE',
  };
  
  FormData formData = FormData.fromMap({
    'file': await MultipartFile.fromFile(event.file.path),
    'snapshot': jsonEncode(snapshot)
  });
  
  try {
    // 接口调用
    await apiService?.postFileEvent(recordId, formData);
  } catch(e) {
    debugPrint(e.toString());
    return '保存失败';
  }
  return '保存成功';
}

PS 上面代码上的 FileState 用于管理数据的状态,比如请求成功,失败,获取数据列表的管理等。

我们假设存在一个 POST 保存文件的接口服务 apiService?.postFileEvent(id, formData),其中 recordId 是记录的 idformData 是放在 body 请求的数据。我们把 description 当作 snapshot 变量中的一个属性进行扩展。

我们可以在 FileState 文件中,定义接口执行的状态,并在页面中监听该状态的更改来展示接口操作成功与否的 Toast。或者,我们也可以通过 EventBus,在 await apiService?.postFileEvent(recordId, formData); 下进行 eventBus.fire 发射状态,在页面中进行监听 eventBus.on().listen。这个根据实际情况来使用。

感谢读者捧场,谢谢

0 人点赞