快速实现一个水印组件

146 阅读3分钟

436698a8afac6cf08c4e680275479192.jpg

实现思路

  • 第一步:选择一个容器,并且这个容器可以覆盖全屏;
  • 第二步:容器必须还能填充背景色(或前景色),并且可以重复的平铺;
  • 第三步:将指定的本文内容绘制成一个图像,这个图像用于第二步中的平铺内容。

实现的难点在于第三步,如何绘制文本,这个内容我将在 _paintUnit 方法中详细给读者介绍。

import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import 'dart:math';

class WaterMark extends StatefulWidget {
  /// 文本内容
  final String text;
  /// 水印的透明度
  final double opacity;
  /// 水印对齐方式
  final Alignment alignment;
  /// 绘制本文的样式
  final TextStyle textStyle;

  const WaterMark({
    super.key,
    required this.text,
    this.opacity = 0.8,
    this.alignment = Alignment.topLeft,
    this.textStyle = const TextStyle(fontSize: 12, color: Colors.black26),
  });

  @override
  _WaterMarkState createState() => _WaterMarkState();
}
class _WaterMarkState extends State<WaterMark> {
  late final Future _future;

  @override
  void initState() {
    _future = Future(_generateImageAsset);
    super.initState();
  }

  Future<MemoryImage> _generateImageAsset() async {
    /// 创建一个 Canvas 进行离屏绘制
    final recorder = ui.PictureRecorder();
    final canvas = Canvas(recorder);

    /// 绘制单元水印并获取其大小
    final size = _paintUnit(
      canvas: canvas,
      text: widget.text,
      textStyle: widget.textStyle,
    );

    /// 完成图形操作的录制。
    /// 返回一张图片,其中包含迄今为止记录的图形操作。调用此函数后,图片记录器和画布对象都无效,无法继续使用。
    final picture = recorder.endRecording();

    /// 根据此图片创建图像。
    /// toImage(width, height) 中的 width * height 决定了图像的宽高。
    /// 图片在0(左)、0(上)、宽度(右)、高度(下)范围内光栅化。超出这些界限的内容将被剪切。
    final img = await picture.toImage(size.width.ceil(), size.height.ceil());

    /// 将图像装换成指定类型的子节数组
    final byteData = await img.toByteData(format: ui.ImageByteFormat.png);

    /// 创建一个 Uint8List 字节编码的缓冲区
    final pngBytes = byteData!.buffer.asUint8List();

    /// 其实就是一个 ImageProvider<MemoryImage>
    return MemoryImage(pngBytes);
  }

  Size _paintUnit({
    required String text,
    required Canvas canvas,
    required TextStyle textStyle,
  }) {
    canvas.save();
    /// 这里其实视同股哟有一个方法
    TextPainter painter = TextPainter(
      textDirection: TextDirection.ltr,
      // textScaler: TextScaler.linear(
      //   MediaQueryData.fromView(WidgetsBinding.instance.platformDispatcher.views.first)
      //       .devicePixelRatio,
      // ),
      /// textScaler 可以根据用户的系统设置,会自动对文本进行放大或缩小,
      /// 也可以设置成根据手机的设备像素比来设置(上方注释掉的部分)。
      textScaler: MediaQueryData.fromView(WidgetsBinding.instance.platformDispatcher.views.first)
          .textScaler,
    );

    painter.text = TextSpan(text: text, style: textStyle);

    painter.layout();

    /// width、height 表示文本的宽度和高度。
    double width = painter.width;
    double height = painter.height;

    /// 取 width、height 最大的作为半径,生成一个 Size,并作为绘制的图像的大小。
    /// 这个尺寸将作为 Canvas 画布的实际大小。
    Size size = Size.fromRadius(max(width, height) / 2 * 1.2);

    /// Canvas 坐标原点先移动、再旋转 45°,最后才是绘制文本内容。
    /// 通过 Canvas 坐标原点的移动,从而使文本最终绘制在矩形的中心位置
    /// 由于绘制区域的大小是一个 size 的正方形,所以旋转 45° 以后,此时文本应该是在对角线上,
    /// 对角线的长度为 size.width * 1.414,所以要想让文本居中,
    /// 就必须要先在 X 方向移动 (size.width * 1.414 - painter.width) / 2 的距离。然后再旋转 45°。
    canvas.translate((size.width * 1.414 - painter.width) / 2, painter.height / 2);
    canvas.rotate(pi / 4);

    painter.paint(canvas, Offset.zero);
    canvas.restore();
    return size;
  }

  @override
  Widget build(BuildContext context) {
    return IgnorePointer(
      child: FutureBuilder(
        future: _future,
        builder: (BuildContext context, AsyncSnapshot snapshot) {
          return Container(
            constraints: const BoxConstraints.expand(),
            decoration: snapshot.connectionState != ConnectionState.done
                ? null
                : BoxDecoration(
                    image: DecorationImage(
                      image: snapshot.data!,
                      opacity: widget.opacity,
                      repeat: ImageRepeat.repeat,
                      alignment: widget.alignment,
                    ),
                  ),
          );
        },
      ),
    );
  }
}

我们通过在最外层包裹 IgnorePointer Widget,从而实现将用户事件透传到水印组件的下面,从而使下方的 UI 界面可以响应用户事件。