HarmonyOS 6.1 水印与安全截图防护 — Canvas叠加实现文档保护

📅 2026/7/10 22:00:52
HarmonyOS 6.1 水印与安全截图防护 — Canvas叠加实现文档保护
去年做过一个企业内部文档查看器需求是防止员工截图外传。产品经理信心满满地说加水印不就行了我泼了盆冷水——水印只能追查泄密源不能阻止截图。后来我们做了两层Canvas水印做追查window.setPrivacyMode做截屏防护。WatermarkDemo把这两层都展示了这篇把水印的实现细节和截屏防护的正确姿势说清楚。Canvas水印的基本原理Stack叠加 透明穿透水印的核心思路是在内容之上叠加一层Canvas绘制半透明旋转文字。但叠加后有个致命问题——Canvas在上层会拦截所有触摸事件导致底下的内容无法点击、无法滚动。解决方法是hitTestBehavior(HitTestMode.Transparent)Stack(){Column(){Text(这是一份内部文档内容).fontSize(16).fontColor(#333333).fontWeight(FontWeight.Medium).margin({top:40,bottom:16})Text(项目名称: HarmonyOS NEXT 技术方案).fontSize(14).fontColor(#666666).margin({bottom:8})Text(负责人: 张三).fontSize(14).fontColor(#666666).margin({bottom:8})Text(创建日期: 2026-07-08).fontSize(14).fontColor(#666666).margin({bottom:8})Text(文件编号: INT-2026-001).fontSize(14).fontColor(#666666)}.width(100%).height(100%).padding(20)if(this.showWatermark){Canvas(this.context2D).width(100%).height(100%).hitTestBehavior(HitTestMode.Transparent).onReady((){this.drawWatermark();})}}.width(90%).height(250).borderRadius(8).backgroundColor(#ffffff).border({width:1,color:#e0e0e0})HitTestMode.Transparent的意思是这个组件自己不参与命中测试触摸事件穿透到下层。还有个HitTestMode.None也类似但None是完全不参与事件分发子组件也收不到Transparent是自身不拦截但子组件可以收到。对于纯Canvas水印来说两者效果一样但如果Canvas上有可交互的子元素Transparent更合适。水印Canvas外面包了if (this.showWatermark)条件渲染开关控制水印显示。不用visibility是因为Canvas不需要时应该从树中移除避免无意义的绘制开销。Canvas绘制水印的完整逻辑drawWatermark是整个水印功能的核心privatesettings:RenderingContextSettingsnewRenderingContextSettings(true);privatecontext2D:CanvasRenderingContext2DnewCanvasRenderingContext2D(this.settings);privatedrawWatermark():void{if(!this.showWatermark){return;}constctx:CanvasRenderingContext2Dthis.context2D;constw:number360;consth:number250;ctx.clearRect(0,0,w,h);constopacity:stringrgba(0, 0, 0, (this.watermarkOpacity/100).toFixed(2));ctx.fillStyleopacity;ctx.font14px sans-serif;constangleRad:numberthis.watermarkAngle*Math.PI/180;constgapX:number160;constgapY:number80;for(lety:number-100;yh100;ygapY){for(letx:number-100;xw100;xgapX){ctx.save();ctx.translate(x60,y30);ctx.rotate(angleRad);ctx.fillText(this.watermarkText,0,0);ctx.restore();}}}这段代码有几个关键点值得展开说。clearRect必须先调用。Canvas不会自动清空上一帧的内容如果参数变了比如水印文字改了、角度调了不清空就直接画新旧内容会叠加在一起变成一团乱码。所以每次drawWatermark开头必须ctx.clearRect(0, 0, w, h)清空画布。rgba透明度的计算。用户通过Slider控制watermarkOpacity5-40的整数但Canvas的fillStyle需要0-1的浮点数所以用this.watermarkOpacity / 100转换。5%透明度时文字几乎看不到40%时文字开始明显遮挡内容这个范围经过实际测试是比较合理的。代码里用.toFixed(2)保证rgba字符串格式正确。旋转前必须translate。ctx.rotate()是围绕坐标原点旋转的。如果你直接rotate再fillText文字会绕Canvas的(0,0)点旋转跑到画布外面去。正确做法是先translate到水印文字应该出现的位置再rotate这样文字就绕自身中心旋转了。每次save/restore配对使用保证transform不影响下一个水印。循环范围超出画布。y从-100开始到h100x从-100到w100这是因为旋转后的水印可能部分在画布外。如果只画0到w/0到h范围内的旋转后画布边缘会出现空白区域没有水印覆盖。多画100像素的余量确保旋转后整个画布都有水印。onReady回调。Canvas的绘制不能在aboutToAppear里做因为此时Canvas还没准备好context2D的方法调了也没效果。必须在onReady回调里绘制这个回调表示Canvas已经初始化完成可以安全使用绑定好的context2D。参数变化时的重绘问题WatermarkDemo提供了水印文字、透明度、旋转角度三个可调参数。问题来了——当你改了参数Canvas不会自动重绘。State变量变化触发的是组件树的更新但Canvas的内容是命令式绘制的不是声明式描述的框架不知道Canvas需要重画。那怎么在参数变化时触发重绘WatermarkDemo用了一个取巧的方法——Canvas包在if (this.showWatermark)里但参数变化不会触发这个if重求值。实际项目中你需要手动调用drawWatermark。一种可靠的方式是监听参数变化后调重绘TextInput({text:this.watermarkText,placeholder:输入水印文字}).onChange((v:string){this.watermarkTextv;this.drawWatermark();})Slider({value:this.watermarkOpacity,min:5,max:40,step:1}).onChange((v:number){this.watermarkOpacityv;this.drawWatermark();})但这里有个时序问题——onChange触发时State还没更新drawWatermark里读到的还是旧值。更稳妥的做法是用WatchWatch(onWatermarkChange)StatewatermarkText:string内部文件 禁止外传;Watch(onWatermarkChange)StatewatermarkOpacity:number15;Watch(onWatermarkChange)StatewatermarkAngle:number-30;privateonWatermarkChange():void{this.drawWatermark();}Watch在State更新后回调此时drawWatermark能读到最新值。但Watch不支持同一个回调函数给多个属性同时使用会重复注册实际写法需要每个属性单独指定Watch回调然后在回调里都调drawWatermark。WatermarkDemo为了简化演示没有加Watch但实际项目必须处理这个重绘问题否则用户改了参数水印不更新直接就提bug了。透明度的平衡太低看不见太高挡内容水印透明度是最难调的参数。5%透明度正常看文档时几乎察觉不到水印截图后放大仔细看能发现——这是理想的追查场景。但5%的水印在深色背景上基本消失如果你的文档内容有深色表格或图片水印形同虚设。30-40%透明度水印非常明显截图后一眼就能看到。但这时候水印已经开始干扰正常阅读了——尤其是大量文字的文档每行字都被半透明的内部文件 禁止外传叠加阅读体验极差。我的经验值是15-20%配合-30度旋转角度。这个区间水印在日常使用中不会太打扰阅读但截图后用图像处理工具调高对比度就能清晰看到。具体值要看文档内容类型——表格多的文档可以稍高纯文字文档要偏低。Demo里默认15%就是这个考量。Slider的max设成40而不是100也是因为超过40%就没法看了。真正的截屏防护window.setPrivacyModeCanvas水印是视觉手段不能阻止截图。用户截图时Canvas内容会被正常捕获到截图中所以水印才有追查价值但截图这个动作本身不会被阻止。如果需要真正的截屏防护要用window.setPrivacyMode(true)。这个API告诉系统这个窗口的内容属于隐私内容截屏时显示黑屏。Android有类似机制FLAG_SECUREiOS没有。import{window}fromkit.ArkUI;// 在Ability的onWindowStageCreate里windowStage.getMainWindow().then((win:window.Window){win.setPrivacyMode(true);});setPrivacyMode(true)后系统截屏、录屏时这个窗口的内容全部显示为黑色。用户截了图也看不到任何内容更不用说外传了。但setPrivacyMode有副作用——整个窗口都变黑包括标题栏、导航栏。如果你只想保护文档内容区域而不影响其他UI就需要把文档放在独立的窗口子窗口里只给子窗口开隐私模式。这会增加复杂度。另外setPrivacyMode不是万能的——硬件级别的截屏比如HDMI采集卡、另一个手机对着拍无法防护。所以实际方案通常是两层叠加setPrivacyMode防软件截屏Canvas水印防物理拍摄后的追查。方案对比总结Demo的对比指南列了三种方案方案性能防截屏适用场景Canvas叠加高否视觉标记追查泄密源StackText低否简单场景少量水印窗口防截屏-是安全场景配合水印StackText方案的低性能是因为大量Text组件的布局计算比Canvas命令式绘制重。Canvas是一次drawText搞定Text组件每个都是独立的布局节点几百个水印Text组件会显著影响渲染性能。所以超过10个水印就选Canvas别用Text叠加。坑点汇总Canvas水印不防截屏— 水印是视觉标记手段只能追查泄密源不能阻止截图行为。需要截屏防护请用window.setPrivacyMode。忘记hitTestBehavior会导致下层内容无法点击— Canvas叠加在上层默认拦截触摸事件必须设hitTestBehavior(HitTestMode.Transparent)。Canvas每次参数变化需手动重绘— clearRect 重画。State变化不会自动触发Canvas重绘需配合Watch或手动调用drawWatermark。透明度太低水印不可见太高遮挡内容— 推荐15-20%区间深色背景文档适当提高。Slider最大值不要超过40%。Canvas绘制必须在onReady回调里— aboutToAppear时Canvas未就绪context2D方法无效。rotate前必须translate— 直接rotate会绕原点旋转文字跑到画布外。先translate到目标位置再rotate。循环绘制范围要超出画布边界— 旋转导致内容偏移只在画布内绘制会导致边缘出现无水印空白区域。写在最后水印是防君子不防小人的手段真正安全要靠setPrivacyMode水印的组合拳透明度和角度的调校是体验和安全之间的走钢丝。