
开篇在HarmonyOS应用中一个常见的需求是给页面内容叠加水印比如企业通讯应用的内部文件预览页面需要在所有人名和敏感区域叠加“机密”字样或者图片分享社区为了保护原创图片不被盗用需要打上透明水印。水印是保护数字内容的低成本、高效果手段HarmonyOS原生Canvas能力让开发者可以在像素级别控制渲染无需依赖第三方库即可实现灵活的水印方案。本系列共2篇第1篇聚焦页面上添加水印——在不影响页面正常交互的前提下通过Canvas绘制旋转文字水印并完美融合到UI层中。你会掌握两种融合方式Stack与overlay并理解水印旋转起点的偏移计算原理。完成这篇你就能在任何页面一步到位添加上水印组件。核心实现基础配置创建水印组件框架水印组件本质上是一个透明的Canvas画布叠加在页面内容之上。初始化一个自定义组件并在其中准备好Canvas上下文。// WatermarkComponent.etsimport{BusinessError}fromkit.BasicServicesKit;Componentstruct WatermarkComponent{// 水印文字内容privatetext:string机密文档;// 水印旋转角度度privateangle:number-30;// 水印文字颜色privatetextColor:stringrgba(0, 0, 0, 0.15);// 水印字号fpprivatefontSize:number20;// 水印行间距绘制完一行水印后向下间距privaterowSpacing:number200;// 水印列间距privatecolSpacing:number300;// Canvas 渲染上下文privatecontext:CanvasRenderingContext2DnewCanvasRenderingContext2D();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(){constctxthis.context;constcanvasWidthctx.width;constcanvasHeightctx.height;// 清空画布ctx.clearRect(0,0,canvasWidth,canvasHeight);// 设置文字样式ctx.fontthis.fontSizefp sans-serif;ctx.fillStylethis.textColor;// 测量单行水印的宽度和高度此处用fontSize估算高度也可精确测量consttextWidthctx.measureText(this.text).width;consttextHeightthis.fontSize;// 近似行高// 将角度转换为弧度constradthis.angle*Math.PI/180;// 计算旋转后的偏移量当角度为负逆时针时需沿y轴正方向偏移letoffsetX0;letoffsetY0;if(this.angle0){offsetXMath.tan(rad)*textHeight;}elseif(this.angle0){offsetYMath.tan(-rad)*textWidth;// 取绝对值偏移}// 保存画布状态ctx.save();// 平移画布使第一个水印不会超出左上角ctx.translate(offsetX,offsetY);// 根据行间距和列间距循环绘制for(lety0;ycanvasHeight;ythis.rowSpacing){for(letx0;xcanvasWidth;xthis.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 层叠容器// 使用示例EntryComponentstruct 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.etsdraw方法实现WatermarkComponent的核心是draw方法通过CanvasRenderingContext2D绘制旋转水印draw(){constctxthis.context;constcanvasWidthctx.width;constcanvasHeightctx.height;// 清空画布ctx.clearRect(0,0,canvasWidth,canvasHeight);// 计算旋转前的单行水印高度文字高度近似于字号consttextHeightthis.fontSize*1.2;// 粗略估算// 计算旋转前的单行水印宽度通过 measureText 获取ctx.font${this.fontSize}fp;consttextWidthctx.measureText(this.text).width;// 将角度转换为弧度constradthis.angle*Math.PI/180;// 计算偏移量以保证第一个水印完整可见letoffsetX0;letoffsetY0;if(this.angle0){offsetXMath.abs(Math.tan(rad))*textHeight;}elseif(this.angle0){offsetYMath.abs(Math.tan(rad))*textWidth;}// 从偏移点开始以行间距、列间距重复填充画布for(letyoffsetY;ycanvasHeighttextHeight;ythis.rowSpacing){for(letxoffsetX;xcanvasWidthtextWidth;xthis.colSpacing){ctx.save();// 平移至当前绘制起点ctx.translate(x,y);// 旋转角度ctx.rotate(rad);// 设置文字样式颜色与字号ctx.fillStylethis.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.etsEntryComponentstruct 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容器将水印组件作为覆盖层放在页面组件上方。这种方式结构清晰控制灵活。EntryComponentstruct 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。EntryComponentstruct 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。