在实际场景中,大多数情况下水印是需要铺满整个屏幕的,如果不需满铺,通常直接用组件组合即可实现。
水印组件WaterMark
通过绘制一个单元水印,然后让它在整个水印组件的背景中重复即可实现满铺效果。因此可以直接使用DecoratedBox,它拥有背景图重复功能。重复的问题处理了,主要问题是绘制单元水印,为了灵活好扩展,定义一个水印画笔接口,这样可以预置一些常用的画笔来满足大多数场景,同时如果有自定义需求也可以通过自定义画笔实现。
定义一个画笔:
//定义水印画笔
abstract class SSLWaterMarkPainter{
//绘制单元水印,完整的水印由单元水印重复平铺后组成,返回值为单元水印占用空间的大小
//[devicePixelRatio]:因为最终要将内容保存为图片,
// 所以在绘制时需要根据屏幕的DPR来放大,以防失真
Size paintUnit(Canvas canvas, double devicePixelRatio);
//是否需要重绘,当画笔状态发生变化时返回true进行重绘
bool shouldRepaint(covariant SSLWaterMarkPainter oldPainter) => true;
}
WaterMark定义:
class SSLWaterMark extends StatefulWidget{
final SSLWaterMarkPainter painter;
final ImageRepeat repeat;
const SSLWaterMark({
Key? key,
this.repeat = ImageRepeat.repeat,
required this.painter,
}):super(key: key);
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return SSLWaterMarkState();
}
}
class SSLWaterMarkState extends State<SSLWaterMark>{
late Future<MemoryImage> memoryImageFuture;
@override
void initState() {
// TODO: implement initState
memoryImageFuture = getWaterMarkImage();
super.initState();
}
//离线缓存逻辑
Future<MemoryImage> getWaterMarkImage() async{
final recorder = PictureRecorder();
final canvas = Canvas(recorder);
//获取屏幕的像素和实际坐标的比例,iOS中的dpt
double dpr = window.devicePixelRatio;
//获取绘制返回的尺寸
final Size size = widget.painter.paintUnit(canvas, dpr);
final picture = recorder.endRecording();
//生成绘制的图片
final img = await picture.toImage(size.width.ceil(), size.height.ceil());
//将图片转成byte数据
final byteData = await img.toByteData(format: ImageByteFormat.png);
final pngBytes = byteData!.buffer.asUint8List();
//缓存图片,传递给渲染层
return MemoryImage(pngBytes);
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return SizedBox.expand(
child: FutureBuilder(
future: memoryImageFuture,
builder: (BuildContext context, AsyncSnapshot snapshot){
if (snapshot.connectionState != ConnectionState.done){
return Container();
}else{
return DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: snapshot.data,//显示缓存的数据
repeat: widget.repeat,//重复类型
alignment: Alignment.topLeft,
)
)
);
}
},
),
);
}
@override
void didUpdateWidget(covariant SSLWaterMark oldWidget) {
// TODO: implement didUpdateWidget
if (widget.painter.runtimeType != oldWidget.painter.runtimeType || widget.painter.shouldRepaint(oldWidget.painter)){
//释放之前的缓存
memoryImageFuture.then((value) => value.evict());
memoryImageFuture = getWaterMarkImage();
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
// TODO: implement dispose
//释放图片缓存
memoryImageFuture.then((value) => value.evict());
super.dispose();
}
}
调用Canvas API后,实际上产生的是一系列绘制指令,绘制指令执行后才能获取绘制结果,而PictureRecorder就是一个绘制指令记录器,它可以记录一段时间内所有绘制指令,通过调用recorder.endRecording()方法来获取记录的绘制指令,该方法返回一个picture对象,是绘制指令的载体,它有一个toImage方法,调用后会执行绘制指令获得绘制的像素结果(ui.image对象),之后就可以将像素结果转为png格式的数据并缓存再MemoryImage中.
定义绘制文本
class SSLTextWaterPainter extends SSLWaterMarkPainter{
double rotate;
TextStyle textStyle;
EdgeInsets padding;
String text;
SSLTextWaterPainter({
Key? key,
double? rotate,
EdgeInsets? padding,
TextStyle? textStyle,
required this.text,
}):assert(rotate == null || rotate >= -90 && rotate <= 90),
rotate = rotate ?? 0,
padding = padding ?? const EdgeInsets.all(10.0),
textStyle = textStyle ?? const TextStyle(
color: Colors.black,
fontSize: 14,
);
@override
Size paintUnit(Canvas canvas, double devicePixelRatio) {
//TODO: implement paintUnit
//使用系统的TextPainter
TextPainter painter = TextPainter(
textDirection: TextDirection.ltr,
textScaleFactor: devicePixelRatio
);
painter.text = TextSpan(text: text,style: textStyle);
painter.layout();
final textWidth = painter.width;
final textHeight = painter.height;
debugPrint("ssl paint width$textWidth height$textHeight");
painter.paint(canvas, Offset.zero);
return Size(textWidth, textHeight);
}
@override
bool shouldRepaint(covariant SSLTextWaterPainter oldPainter) {
// TODO: implement shouldRepaint
return oldPainter.text != text || oldPainter.textStyle != textStyle;
}
}
测试示例:
class SSLTextWaterRoute extends StatelessWidget{
const SSLTextWaterRoute({Key? key}):super(key: key);
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: const Text("SSL Text water"),
),
body: Stack(
children: [
IgnorePointer(
child: SSLWaterMark(
painter: SSLTextWaterPainter(
text: "ssl white",
textStyle: const TextStyle(color: Colors.lightGreen),
)
),
),
],
),
);
}
}
此时一个简单的水印功能已经实现了,但是实际中可能都需要对水印进行旋转。下面进行优化。
带角度的水印
先看一张图,看下旋转后宽高的计算:
上代码:
class TextWaterMarkPainter extends SSLWaterMarkPainter{
double rotate;
TextStyle textStyle;
EdgeInsets padding;
String text;
TextWaterMarkPainter({
Key? key,
double? rotate,
EdgeInsets? padding,
TextStyle? textStyle,
required this.text,
}): assert(rotate == null || rotate >= -90 && rotate <= 90),
rotate = rotate ?? 0,
padding = padding ?? const EdgeInsets.all(10.0),
textStyle = textStyle ?? const TextStyle(color: Colors.black,fontSize: 14);
@override
Size paintUnit(Canvas canvas, double devicePixelRatio) {
// TODO: implement paintUnit
TextPainter painter = TextPainter(
textDirection: TextDirection.ltr,
textScaleFactor: devicePixelRatio,//根据屏幕放大
);
painter.text = TextSpan(text: text,style: textStyle);
painter.layout();
//获取未旋转的文本宽高
final textWidth = painter.width;
final textHeight = painter.height;
final radians = math.pi * rotate / 180;
final orgSin = math.sin(radians);
final sin = orgSin.abs();
final cos = math.cos(radians).abs();
final width = textWidth*cos;
final height = textWidth*sin;
final adjustWidth = textHeight * sin;
final adjustHeight = textHeight * cos;
if (orgSin >= 0){
canvas.translate(adjustWidth + padding.left, padding.top);
}else{
canvas.translate(padding.left, height + padding.top);
}
canvas.rotate(radians);
painter.paint(canvas, Offset.zero);
//计算最终需要绘制宽高
Size size = Size(width + adjustWidth + padding.horizontal, height + adjustHeight + padding.vertical);
debugPrint("ssl paint size $size\n");
return size;
}
@override
bool shouldRepaint(covariant TextWaterMarkPainter oldPaint) {
// TODO: implement shouldRepaint
// return super.shouldRepaint(oldPaint);
return oldPaint.rotate != rotate ||
oldPaint.textStyle != textStyle ||
oldPaint.text != text ||
oldPaint.padding != padding;
}
}
测试代码:
class TestTextWater extends StatefulWidget{
const TestTextWater({Key? key}):super(key: key);
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return TestTextWaterState();
}
}
class TestTextWaterState extends State<TestTextWater>{
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: const Text("SSL Water Mark"),
),
body:
Stack(
children: [
wTextPainterTest(),
Center(
child: ElevatedButton(onPressed: (){
debugPrint("Chick Button");
}, child: const Text("Chick Button")
),
),
IgnorePointer(
child: SSLWaterMark(
painter: TextWaterMarkPainter(
text: "SSL White Monkey",
textStyle: const TextStyle(
fontSize: 15,
color: Colors.purple,
fontWeight: FontWeight.w200
),
rotate: -20,
),
),
),
],
),
);
}
Widget wTextPainterTest() {
// 我们想提前知道 Text 组件的大小
Text text = const Text('SSL Water Mark', style: TextStyle(fontSize: 15));
// 使用 TextPainter 来测量
TextPainter painter = TextPainter(textDirection: TextDirection.ltr);
// 将 Text 组件文本和样式透传给TextPainter
painter.text = TextSpan(text: text.data,style:text.style);
// 开始布局测量,调用 layout 后就能获取文本大小了
painter.layout();
// 自定义组件 AfterLayout 可以在布局结束后获取子组件的大小,我们用它来验证一下
// TextPainter 测量的宽高是否正确
return SSLAfterLayout(
callback: (SSLRenderAfterLayout value) {
// 输出日志
debugPrint('text size(painter): ${painter.size}');
debugPrint('text size(after layout): ${value.size}');
},
child: text,
);
}
}
再次升级一下,实现交错文本水印:
class SSLStaggerTextPainter extends SSLWaterMarkPainter{
String text1;
String text2;
//旋转角度
double? rotate;
//文本风格
TextStyle? textStyle;
//边距
EdgeInsets? padding1;
EdgeInsets? padding2;
//文本排列方向
Axis staggerAxis;
SSLStaggerTextPainter({
required this.text1,
this.padding1,
this.padding2 = const EdgeInsets.all(30),
this.rotate,
this.textStyle,
this.staggerAxis = Axis.vertical,
String? text2,
}):text2 = text2 ?? text1;
@override
ui.Size paintUnit(ui.Canvas canvas, double devicePixelRatio) {
// TODO: implement paintUnit
final TextWaterMarkPainter painter = TextWaterMarkPainter(
text: text1,
padding: padding1,
rotate: rotate ?? 0,
textStyle: textStyle
);
//绘制第一个文本水印前保存画布状态,因为再绘制过程中可能会平移或旋转画布
canvas.save();
//绘制第一个文本水印
final size = painter.paintUnit(canvas, devicePixelRatio);
//绘制完毕后回复画布状态
canvas.restore();
bool vertical = staggerAxis == Axis.vertical;
canvas.translate(vertical?0:size.width, vertical? size.height:0);
painter
..padding = padding2!
..text = text2;
final size2 = painter.paintUnit(canvas, devicePixelRatio);
return Size(vertical? math.max(size.width, size2.width) : size.width + size2.width, vertical?size.height + size2.height:math.max(size.height, size2.height));
}
@override
bool shouldRepaint(covariant SSLStaggerTextPainter oldPainter) {
// TODO: implement shouldRepaint
return oldPainter.rotate != rotate ||
oldPainter.text1 != text1 ||
oldPainter.text2 != text2 ||
oldPainter.staggerAxis != staggerAxis ||
oldPainter.padding1 != padding1 ||
oldPainter.padding2 != padding2 ||
oldPainter.textStyle != textStyle;
}
}
注意:
- 在绘制第一个文本之前需要先调用canvas.save保存画布状态,因为绘制过程中可能会平移或旋转画布,在绘制第二个文本之前回复画布状态,并将Canvas平移至第二个文本水印的起始绘制点。
- 两个文本可以沿水平方向排列也可以沿竖直方向排列,不同的排列规则会影响最终水印单元的大小。
- 交错的偏移通过padding2来指定。