HarmonyOS实战-水印添加 - 第1篇:页面上添加水印

开篇
在HarmonyOS应用中,一个常见的需求是给页面内容叠加水印:比如企业通讯应用的内部文件预览页面,需要在所有人名和敏感区域叠加“机密”字样;或者图片分享社区为了保护原创图片不被盗用,需要打上透明水印。水印是保护数字内容的低成本、高效果手段,HarmonyOS原生Canvas能力让开发者可以在像素级别控制渲染,无需依赖第三方库即可实现灵活的水印方案。
本系列共2篇,第1篇聚焦页面上添加水印——在不影响页面正常交互的前提下,通过Canvas绘制旋转文字水印,并完美融合到UI层中。你会掌握两种融合方式(Stack与overlay),并理解水印旋转起点的偏移计算原理。完成这篇,你就能在任何页面一步到位添加上水印组件。
核心实现
基础配置:创建水印组件框架
水印组件本质上是一个透明的Canvas画布,叠加在页面内容之上。初始化一个自定义组件,并在其中准备好Canvas上下文。
// WatermarkComponent.ets
import { BusinessError } from '@kit.BasicServicesKit';
@Component
struct WatermarkComponent {
// 水印文字内容
private text: string = '机密文档';
// 水印旋转角度(度)
private angle: number = -30;
// 水印文字颜色
private textColor: string = 'rgba(0, 0, 0, 0.15)';
// 水印字号(fp)
private fontSize: number = 20;
// 水印行间距(绘制完一行水印后向下间距)
private rowSpacing: number = 200;
// 水印列间距
private colSpacing: number = 300;
// Canvas 渲染上下文
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D();
build() {
// Canvas 尺寸需要填满父容器,这里使用 100% 宽高
Canvas(this.context)
.width('100%')
.height('100%')
// 水印组件不响应触摸事件,让下层页面正常交互
.hitTestBehavior(HitTestBehavior.Transparent)
.onReady(() => {
this.draw();
})
}
// 核心绘制方法(后续实现)
draw() {
// 待实现
}
}
注意事项
hitTestBehavior(HitTestBehavior.Transparent)是关键属性:让水印组件不拦截触摸事件,用户依然能点击、滑动下层页面。如果误设为默认值或Block,水印区域会导致下层页面无法响应触摸。onReady事件在 Canvas 初始化完成或尺寸变化时触发,是启动绘制的最佳时机,但要确保在draw()中已获取到正确的ctx.width和ctx.height。- 所有水印参数(文字、角度、颜色)都作为组件属性暴露,方便外部配置。如需动态修改,可在状态变化时重新调用
draw()。
核心逻辑:旋转水印的坐标偏移计算与绘制
水印通常带有一定倾斜角度(如-30°)以达到更好的防伪效果。当 Canvas 旋转后,第一个水印的绘制起点需要偏移,否则会被画布边缘裁剪。偏移量根据旋转方向计算:
- 角度 > 0(顺时针旋转):起点沿 x 轴平移
tan(θ) * 水印高度 - 角度 < 0(逆时针旋转):起点沿 y 轴平移
tan(θ) * 水印宽度
我们将偏移计算和循环绘制逻辑封装到 draw() 方法中。
draw() {
const ctx = this.context;
const canvasWidth = ctx.width;
const canvasHeight = ctx.height;
// 清空画布
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
// 设置文字样式
ctx.font = this.fontSize + 'fp sans-serif';
ctx.fillStyle = this.textColor;
// 测量单行水印的宽度和高度(此处用fontSize估算高度,也可精确测量)
const textWidth = ctx.measureText(this.text).width;
const textHeight = this.fontSize; // 近似行高
// 将角度转换为弧度
const rad = this.angle * Math.PI / 180;
// 计算旋转后的偏移量:当角度为负(逆时针)时,需沿y轴正方向偏移
let offsetX = 0;
let offsetY = 0;
if (this.angle > 0) {
offsetX = Math.tan(rad) * textHeight;
} else if (this.angle < 0) {
offsetY = Math.tan(-rad) * textWidth; // 取绝对值偏移
}
// 保存画布状态
ctx.save();
// 平移画布,使第一个水印不会超出左上角
ctx.translate(offsetX, offsetY);
// 根据行间距和列间距循环绘制
for (let y = 0; y < canvasHeight; y += this.rowSpacing) {
for (let x = 0; x < canvasWidth; x += this.colSpacing) {
ctx.save();
// 移动到当前绘制点
ctx.translate(x, y);
// 旋转画布
ctx.rotate(rad);
// 绘制水印文字(以(0,0)为基准)
ctx.fillText(this.text, 0, 0);
ctx.restore();
}
}
// 恢复画布状态
ctx.restore();
}
关键点说明
- 偏移量的计算:当角度为负(逆时针)时,第一个水印的左上角如果直接放在
(0,0)并旋转,文字会向左上方移出画布。通过offsetY = Math.tan(-rad) * textWidth将起点向下平移,保证文字完整可见。同理,正角度偏移 x 方向。 - 使用
ctx.save()/ctx.restore()包裹每个水印的绘制,避免旋转和位移影响后续循环。 this.rowSpacing和this.colSpacing控制水印间隔,可根据实际效果调整。如果希望水印更密集,可减小这两个值。
融合到页面:两种方式
创建好水印组件后,需要将其插入页面布局。推荐两种方式:
方式一:Stack 层叠容器
// 使用示例
@Entry
@Component
struct DocumentPage {
build() {
Stack() {
// 实际页面内容
Column() {
Text('文档预览区域')
.fontSize(30)
.width('100%')
.height('100%')
.textAlign(TextAlign.Center)
}
.width('100%')
.height('100%')
// 水印组件叠加在上层
WatermarkComponent()
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
}
}
方式二:自定义 overlay
如果需要更精细的覆盖(如只在特定区域显示水印),可以使用 overlay 属性:
Column() {
// 页面内容
}
.width('100%')
.height('100%')
.overlay({
builder: () => {
WatermarkComponent()
.width('100%')
.height('100%')
},
align: Alignment.Center
})
注意事项
- 使用
Stack时,子组件按声明顺序从下到上叠加,水印组件必须放在最后。 overlay方式中,水印组件会在自身区域对齐,如果Column内部有滚动,水印会跟随内容滚动。如需固定水印,建议用Stack并固定水印组件于外层。
结尾
你会选择哪种方式集成水印组件?在实际项目中,Stack 更通用,overlay 适合局部覆盖。动手试试,注意偏移量计算是否正确,以及 hitTestBehavior 是否设置。遇到水印被裁剪或交互被阻挡,优先检查这两个地方。
在HarmonyOS应用开发中,为页面添加水印是常见的需求,例如保护版权、标识状态、防止截图泄露等。通过Canvas绘制并利用Stack布局叠加是一种高效灵活的实现方式。本文直接讲解WatermarkComponent中draw方法的实现细节,以及如何通过Stack将水印覆盖到页面内容之上。
WatermarkComponent.ets:draw方法实现
WatermarkComponent的核心是draw方法,通过CanvasRenderingContext2D绘制旋转水印:
draw() {
const ctx = this.context;
const canvasWidth = ctx.width;
const canvasHeight = ctx.height;
// 清空画布
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
// 计算旋转前的单行水印高度(文字高度近似于字号)
const textHeight = this.fontSize * 1.2; // 粗略估算
// 计算旋转前的单行水印宽度(通过 measureText 获取)
ctx.font = `${this.fontSize}fp`;
const textWidth = ctx.measureText(this.text).width;
// 将角度转换为弧度
const rad = this.angle * Math.PI / 180;
// 计算偏移量以保证第一个水印完整可见
let offsetX = 0;
let offsetY = 0;
if (this.angle > 0) {
offsetX = Math.abs(Math.tan(rad)) * textHeight;
} else if (this.angle < 0) {
offsetY = Math.abs(Math.tan(rad)) * textWidth;
}
// 从偏移点开始,以行间距、列间距重复填充画布
for (let y = offsetY; y < canvasHeight + textHeight; y += this.rowSpacing) {
for (let x = offsetX; x < canvasWidth + textWidth; x += this.colSpacing) {
ctx.save();
// 平移至当前绘制起点
ctx.translate(x, y);
// 旋转角度
ctx.rotate(rad);
// 设置文字样式(颜色与字号)
ctx.fillStyle = this.textColor;
ctx.font = `${this.fontSize}fp`;
// 绘制水印文字,位置为 (0, 0),因为已通过 translate 定位
ctx.fillText(this.text, 0, 0);
ctx.restore();
}
}
}
关键点说明
ctx.measureText()用于精确获取文本宽度,保证偏移计算准确。- 坐标轴变换使用
save()/restore()成对出现,避免状态污染。 - 双层循环覆盖整个画布区域,行/列间距可根据实际视觉效果调整。
- 偏移量
offsetX / offsetY确保了旋转后第一个水印不会被画布左上角裁剪。
实用提示
- 偏移量的计算依赖于
textWidth和textHeight,这些值在旋转前测量。如果文字方向改变或使用多行文本,需要相应调整偏移公式。 - 双层循环的范围覆盖到
canvasHeight + textHeight和canvasWidth + textWidth,确保边缘不会因旋转角度产生空白。行间距和列间距建议根据水印密集程度预留一定重叠,避免出现大面积无文字区域。
完整组件与页面集成(Stack方式)
现在将水印组件与一个示例页面融合。使用 Stack 布局将水印覆盖在内容上方。
// MainPage.ets
@Entry
@Component
struct MainPage {
build() {
Stack() {
// 下层:页面实际内容(示例为一段文字说明)
Column() {
Text('这是应用主页面,可正常交互')
.fontSize(24)
.margin(20)
Button('点击测试')
.onClick(() => {
console.info('测试交互正常');
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
集成要点
- WatermarkComponent需作为
Canvas子组件放在Stack上层,并用.hitTestBehavior(HitTestBehavior.None)允许点击穿透,否则会遮挡下层交互。 - 水印的宽高建议设置为
'100%',与父容器尺寸一致,确保覆盖完整。 - 如果水印需要根据页面滚动保持固定位置,可以结合
Scroll或position属性调整。
注意事项
- Stack的图层顺序:先写内容组件(下层),后写水印组件(上层),否则水印会被覆盖。
- 使用
Stack时,水印组件默认不可交互,若需水印区域响应点击(如动态更改配置),需单独设置hitTestBehavior。
如果你想在不同页面复用该水印组件,可以将WatermarkComponent封装为自定义组件并通过@Prop传递参数。你通常使用哪种方式控制水印的显隐或动态更新?欢迎留言交流。
使用 Stack 或 overlay 将水印组件集成到页面
上一节实现了 WatermarkComponent,它基于 Canvas 绘制旋转水印,并通过 hitTestBehavior: Transparent 确保不拦截触摸事件。在实际项目中,需要将这个水印组件叠加到已有页面上,同时保持底层交互正常。
推荐使用 Stack 容器,将水印组件作为覆盖层放在页面组件上方。这种方式结构清晰,控制灵活。
@Entry
@Component
struct MainPage {
build() {
Stack() {
// 底层:页面内容
Column() {
Text('Hello HarmonyOS')
.fontSize(28)
.margin(20)
Button('点击测试')
.onClick(() => {
console.info('测试交互正常');
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
// 上层:水印组件
WatermarkComponent()
// 水印组件本身已设置 hitTestBehavior: Transparent
}
.width('100%')
.height('100%')
}
}
注意事项:
WatermarkComponent内部通过 Canvas 绘制,且已显式设置hitTestBehavior: Transparent,因此无需在外部额外设置。- Stack 中子组件按添加顺序叠加,确保水印组件在最后一点视觉上位于最上层。
备选方案:使用 overlay 属性
如果不想手动嵌套 Stack,可以利用容器组件的 .overlay() 属性将水印挂载为浮层。但需注意,overlay 内部会自动包裹一层 Column 父容器,该父容器默认会拦截触摸事件,必须手动将其 hitTestBehavior 设置为 Transparent。
@Entry
@Component
struct MainPageOverlay {
build() {
Column() {
Text('使用 overlay 方式添加水印')
.fontSize(24)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.overlay((builder: CustomBuilder) => {
builder() // builder 返回 WatermarkComponent,但被包在 Column 中
})
// 这里的 overlay 默认会生成一个 Column 容器,需手动设置 hitTestBehavior
// 但当前 API 中 overlay 不支持直接设置内部容器属性,因此不推荐使用。
}
}
实际建议:
- 直接使用
Stack方式更清晰可控,overlay方式因无法直接控制内部容器的触摸穿透,容易导致水印阻挡点击。除非后续 HarmonyOS 提供相关 API,否则应避免使用。 - 若需要全局水印,可将
WatermarkComponent的创建逻辑抽取为全局@Builder,然后在每个页面的根容器上通过Stack叠加。
[截图: 页面叠加水印后的效果]
运行验证
- 将
WatermarkComponent放在MainPage的Stack中,运行应用。 - 页面应显示半透明的旋转水印文字(默认
机密文档,倾斜 -30°)。 - 点击页面中的“点击测试”按钮,控制台应输出日志,证明水印未阻挡点击事件。
- 若有需要,可调整
angle、textColor、rowSpacing等参数观察水印排列变化。
预期表现:底层页面正常交互,水印覆盖渲染但不干扰触摸。

小结与预告
本篇完成了页面上添加水印的核心实现:
✅ 基于 Canvas 的自定义水印组件,支持旋转角度和起点偏移
✅ 通过 Stack 集成,并设置 hitTestBehavior 确保交互穿透
✅ 对比 overlay 的局限性,推荐优选 Stack 方案
下一篇将进入 图片上添加水印 和 PDF 文档添加水印 场景。你将学到如何从文件或相机获取 pixelMap,通过 Canvas 合成水印并保存为新图片,以及使用 PDF 库在文档页面添加水印。届时,本系列的水印工具箱将覆盖所有主流场景。
系列文章所有代码可直接在 DevEco Studio 中运行,跟随实战,上线一个带完整水印功能的 App。
更多推荐



所有评论(0)