写了个水印组件,有缘人直接拿走。 (不是故意水贴,有时间回来再编辑)
简单介绍
基于 InheritedWidget、SingleChildRenderObjectWidget 实现。
水印原理
-
将设定的旋转角度、控件尺寸带入三角函数可求得水印图层尺寸。
-
建立图层绘制水印,然后同样基于三角函数求得坐标,随后进行旋转、位移。
-
裁剪并合并图层。
-
局部水印
Widget build(BuildContext context) {
return YTextWatermark(
text: '我的水印',
spacing: 22,
runSpacing: 44,
child: Container(color: Colors.yellow, width: 200, height: 600),
);
}
-
全局水印
如果希望水印覆盖整个应用,则需要使用 YTextWatermark 包裹 MyApp。
void main() {
runApp(
const YTextWatermark(
child: MyApp(),
),
);
}
可以在子节点中调用内置函数更新水印
void _update(BuildContext context) {
YTextWatermark.of(context).update(
text: '我是新水印', // 文本
// style: const TextStyle(color: Colors.red), // 文本样式
// clipOffset: Offset.zero, // 裁切偏移
// layerOffset: Offset.zero, // 画布偏移
// angle: 45.0, // * 旋转角度
// spacing: 100.0, // * x 轴字体间距
// runSpacing: 100.0, // * y 轴字体间距
);
}
可以在子节点中调用内置函数清除水印
void _clear(BuildContext context) {
YTextWatermark.of(context).clear();
}
可以在子节点中调用内置函数基于appBar裁剪水印
void _clip(BuildContext context) {
// 此处使用 Scaffold 组件中 body 所对应的 context
// 基于 body 相对屏幕的偏移来裁剪水印,从而去掉 AppBar 上的水印。
final RenderBox paperRender = context.findRenderObject()! as RenderBox;
Offset offset = paperRender.localToGlobal(Offset.zero);
YTextWatermark.of(context).update(clipOffset: offset);
}
源码:
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
const _defaultWatermarkTextStyle = TextStyle(color: Colors.black12, fontSize: 12, fontWeight: FontWeight.w400);
class YTextWatermark extends StatefulWidget {
const YTextWatermark({
Key? key,
required this.child,
this.text,
this.style,
this.spacing = 55.0,
this.runSpacing = 55.0,
this.clipOffset = Offset.zero,
this.layerOffset = Offset.zero,
this.angle = 45,
this.clipBehavior = Clip.hardEdge,
}) : super(key: key);
final String? text;
final TextStyle? style;
final double spacing;
final double runSpacing;
final Offset clipOffset;
final Offset layerOffset;
final double angle;
final Clip clipBehavior;
final Widget child;
@override
State<YTextWatermark> createState() => YTextWatermarkState();
static YTextWatermarkState of(BuildContext context) {
final _YTextWatermarkScope scope = _YTextWatermarkScope.of(context);
return scope.watermarkState;
}
}
class YTextWatermarkState extends State<YTextWatermark> {
String? text;
late TextStyle style;
late double spacing;
late double runSpacing;
late Offset clipOffset;
late Offset layerOffset;
late double angle;
late Clip clipBehavior;
@override
void initState() {
this
..text = widget.text
..style = _defaultWatermarkTextStyle.merge(widget.style)
..spacing = widget.spacing
..runSpacing = widget.runSpacing
..clipBehavior = widget.clipBehavior
..clipOffset = widget.clipOffset
..layerOffset = widget.layerOffset
..angle = widget.angle
..clipBehavior = widget.clipBehavior;
super.initState();
}
void update({
String? text,
TextStyle? style,
double? spacing,
double? runSpacing,
Offset? clipOffset,
Offset? layerOffset,
double? angle,
Clip? clipBehavior,
}) {
setState(() {
if (text != null) this.text = text;
if (style != null) this.style = this.style.merge(style);
if (spacing != null) this.spacing = spacing;
if (runSpacing != null) this.runSpacing = runSpacing;
if (clipOffset != null) this.clipOffset = clipOffset;
if (layerOffset != null) this.layerOffset = layerOffset;
if (angle != null) this.angle = angle;
if (clipBehavior != null) this.clipBehavior = clipBehavior;
});
}
void clear() {
setState(() {
text = null;
});
}
@override
Widget build(BuildContext context) {
return _YTextWatermarkScope(
watermarkState: this,
child: RepaintBoundary(
child: _YTextLayer(
textSpan: TextSpan(text: text, style: style),
spacing: spacing,
runSpacing: runSpacing,
clipOffset: clipOffset,
offset: layerOffset,
angle: angle,
clipBehavior: clipBehavior,
child: RepaintBoundary(
child: widget.child,
),
),
),
);
}
}
class _YTextWatermarkScope extends InheritedWidget {
final YTextWatermarkState watermarkState;
const _YTextWatermarkScope({
Key? key,
required this.watermarkState,
required Widget child,
}) : super(key: key, child: child);
@override
bool updateShouldNotify(covariant _YTextWatermarkScope oldWidget) {
return watermarkState != oldWidget.watermarkState;
}
static _YTextWatermarkScope of(BuildContext context) {
final result = context.dependOnInheritedWidgetOfExactType<_YTextWatermarkScope>();
assert(result != null, '在上层组件树种未找到 YTextWatermark 组件,请检查代码~。');
return result!;
}
}
extension _RadiansExtension on double {
double get radians => this * pi / 180;
}
class _YTextLayer extends SingleChildRenderObjectWidget {
const _YTextLayer({
Key? key,
required this.textSpan,
this.spacing = 140.0,
this.runSpacing = 140.0,
this.clipOffset = Offset.zero,
this.offset = Offset.zero,
this.angle = 45,
this.clipBehavior = Clip.hardEdge,
required Widget child,
}) : super(key: key, child: child);
final TextSpan textSpan;
final double spacing;
final double runSpacing;
final Offset clipOffset;
final Offset offset;
final double angle;
final Clip clipBehavior;
@override
RenderObject createRenderObject(BuildContext context) => _RenderYTextLayer(
textSpan: textSpan,
spacing: spacing,
runSpacing: runSpacing,
clipOffset: clipOffset,
layerOffset: offset,
angle: angle,
clipBehavior: clipBehavior,
);
@override
void updateRenderObject(BuildContext context, covariant _RenderYTextLayer renderObject) {
renderObject
..textSpan = textSpan
..spacing = spacing
..runSpacing = runSpacing
..clipOffset = clipOffset
..layerOffset = offset
..angle = angle
..clipBehavior = clipBehavior;
renderObject.markNeedsPaint();
}
}
class _RenderYTextLayer extends RenderConstrainedBox {
_RenderYTextLayer({
required this.textSpan,
required this.spacing,
required this.runSpacing,
required this.clipOffset,
required this.angle,
required this.clipBehavior,
required this.layerOffset,
}) : super(additionalConstraints: const BoxConstraints.expand());
TextSpan textSpan;
double spacing;
double runSpacing;
Offset clipOffset;
Offset layerOffset;
double angle;
Clip clipBehavior;
@override
bool hitTestSelf(Offset position) => false;
@override
void performLayout() {
child!.layout(constraints, parentUsesSize: true);
size = child!.size;
}
void paintWatermark(PaintingContext context, Offset offset) {
final textPainter = TextPainter(
text: textSpan,
textDirection: TextDirection.rtl,
textAlign: TextAlign.end,
textWidthBasis: TextWidthBasis.longestLine,
maxLines: 3,
)..layout();
final double x1 = size.height * sin(angle.radians);
final double x2 = size.width * sin((90 - angle).radians);
final double y1 = size.height * sin((90 - angle).radians);
final double y2 = size.width * sin(angle.radians);
context.canvas.save();
context.canvas
..translate(offset.dx, offset.dy)
..rotate(-angle.radians)
..translate(-x1, 0);
final int xCount = (x1 + x2) ~/ (textPainter.width + spacing) + 1;
final int yCount = (y1 + y2) ~/ (textPainter.height + runSpacing) + 1;
for (int x = 0; x < xCount; x++) {
for (int y = 0; y < yCount; y++) {
final double dxd = (spacing / 2 + textPainter.width / 2) * (y % 2);
final double dx = (textPainter.width + spacing) * x - dxd;
final double dy = (textPainter.height + runSpacing) * y;
textPainter.paint(context.canvas, Offset(dx, dy));
}
}
context.canvas.restore();
}
@override
void paint(PaintingContext context, Offset offset) {
super.paint(context, offset);
if (textSpan.text?.isEmpty ?? true) {
return;
}
context.pushClipRect(
needsCompositing,
offset + layerOffset,
Rect.fromLTWH(
clipOffset.dx,
clipOffset.dy,
size.width,
size.height,
),
paintWatermark,
clipBehavior: clipBehavior,
);
}
}