Flutter 水印组件WaterMark

83 阅读2分钟

水印组件 WaterMark 的定义:

class WaterMark extends StatefulWidget {
  WaterMark({
    Key? key,
    this.repeat = ImageRepeat.repeat,
    required this.painter,
  }) : super(key: key);

  /// 单元水印画笔
  final WaterMarkPainter painter;

  /// 单元水印的重复方式
  final ImageRepeat repeat;

  @override
  State<WaterMark> createState() => _WaterMarkState();
}

State 实现:

class _WaterMarkState extends State<WaterMark> {
  late Future<MemoryImage> _memoryImageFuture;

  @override
  void initState() {
    // 缓存的是promise
    _memoryImageFuture = _getWaterMarkImage();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox.expand( // 水印尽可能大
      child: FutureBuilder(
        future: _memoryImageFuture,
        builder: (BuildContext context, AsyncSnapshot snapshot) {
          if (snapshot.connectionState != ConnectionState.done) {
            // 如果单元水印还没有绘制好先返回一个空的Container
            return Container(); 
          } else {
            // 如果单元水印已经绘制好,则渲染水印
            return DecoratedBox(
              decoration: BoxDecoration(
                image: DecorationImage(
                  image: snapshot.data, // 背景图,即我们绘制的单元水印图片
                  repeat: widget.repeat, // 指定重复方式
                  alignment: Alignment.topLeft,
                ),
              ),
            );
          }
        },
      ),
    );
  }

  @override
  void didUpdateWidget(WaterMark oldWidget) {
   ... //待实现
  }

  // 离屏绘制单元水印并将绘制结果转为图片缓存起来
  Future<MemoryImage> _getWaterMarkImage() async {
    ... //待实现
  }

  @override
  void dispose() {
   ...// 待实现
  }
}

通过 DecoratedBox 来实现背景图重复,同时我们在组件初始化时开始进行离屏绘制单元水印,并将结果缓存在 MemoryImage 中,因为离屏绘制是一个异步任务,所以直接缓存 Future 即可

需要注意,当组件重新build时,如果画笔配置发生变化,则需要重新绘制单元水印并缓存新的绘制结果:

 @override
  void didUpdateWidget(WaterMark oldWidget) {
     // 如果画笔发生了变化(类型或者配置)则重新绘制水印
    if (widget.painter.runtimeType != oldWidget.painter.runtimeType ||
        widget.painter.shouldRepaint(oldWidget.painter)) {
      //先释放之前的缓存
      _memoryImageFuture.then((value) => value.evict());
      //重新绘制并缓存
      _memoryImageFuture = _getWaterMarkImage();
    }
    super.didUpdateWidget(oldWidget);  
  }

注意,在重新绘制单元水印之前要先将旧单元水印的缓存清理掉,清理缓存可以通过调用 MemoryImage 的 evict 方法。同时,当组件卸载时,也要释放缓存:

 @override
  void dispose() {
    //释放图片缓存
    _memoryImageFuture.then((value) => value.evict());
    super.dispose();
  }

离屏绘制:

接下来就需要重新绘制单元水印了,调用 _getWaterMarkImage() 方法即可,该方法的功能是离屏绘制单元水印并将绘制结果转为图片缓存起来

// 离屏绘制单元水印并将绘制结果保存为图片缓存起来
Future<MemoryImage> _getWaterMarkImage() async {
  // 创建一个 Canvas 进行离屏绘制,细节和原理请查看本书后面14.5节。
  final recorder = ui.PictureRecorder();
  final canvas = Canvas(recorder);
  // 绘制单元水印并获取其大小
  final size = widget.painter.paintUnit(
    canvas,
    MediaQueryData.fromWindow(ui.window).devicePixelRatio,
  );
  final picture = recorder.endRecording();
  //将单元水印导为图片并缓存起来
  final img = await picture.toImage(size.width.ceil(), size.height.ceil());
  final byteData = await img.toByteData(format: ui.ImageByteFormat.png);
  final pngBytes = byteData!.buffer.asUint8List();
  return MemoryImage(pngBytes);
}