最近在做笔记类应用时,遇到一个头疼的需求:防止用户内容被非法截图传播。思来想去,加水印是个直接有效的方案。研究了 HarmonyOS 的开发文档后,发现用 Canvas 配合布局组件能轻松实现动态水印效果。今天就来聊聊如何给笔记页面加上「会呼吸」的用户专属水印,顺便分享几个开发时踩过的坑。

一、需求拆解:什么样的水印防截图最有效?

我们的目标很明确:在笔记浏览页面覆盖一层半透明水印,内容包含用户 ID+实时时间戳,且满足以下条件:

  • 斜向排列:防止截图后通过简单裁剪去除
  • 动态更新:每分钟刷新时间戳,增加追踪难度
  • 性能无感:不影响页面滑动和交互
  • 全局覆盖:适配不同屏幕尺寸和旋转方向

二、核心实现:用canvas实现动态水印

1. 搭建水印组件骨架

首先封装一个 UserWatermark 组件,基于 HarmonyOS 的 Canvas 实现自绘。这里有个关键细节:通过 hitTestBehavior 设置水印层透明,避免阻挡用户点击笔记内容。

// components/Watermark.ets
@Component
export struct UserWatermark {
@State userId = 'user001' // 从账号服务获取的动态用户ID
private context = new CanvasRenderingContext2D(new RenderingContextSettings(true))
private timestamp = new Date().toLocaleString() // 实时时间戳 build() {
Canvas(this.context)
.width('100%')
.height('100%')
.hitTestBehavior(HitTestMode.Transparent) // 重点!不影响触摸事件
.onReady(() => this.drawWatermark())
} // 初始化绘制
private drawWatermark() {
this.updateTimestamp() // 先更新时间
this.context.clearRect(0, 0, this.context.width, this.context.height)
this.setWatermarkStyle()
this.drawWatermarkGrid()
}
}

2. 动态时间戳实现:每分钟刷新一次

为了避免高频重绘影响性能,选择每分钟更新一次时间戳。这里用 setInterval 配合状态变量触发重绘:

// 组件生命周期钩子
aboutToAppear() {
this.updateTimestamp() // 初始化时间
setInterval(() => {
this.timestamp = new Date().toLocaleString() // 更新时间
this.drawWatermark() // 触发画布重绘
}, 60 * 1000) // 每分钟执行一次
} // 时间格式化方法(可根据需求调整)
private updateTimestamp() {
const now = new Date()
this.timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
}

3. 斜向水印的坐标计算「玄学」

实现斜向排列的关键是坐标系旋转和平移。这里踩过最大的坑是旋转后原点位置的变化,调试了半小时才发现需要将原点移到左下角:

private drawWatermarkGrid() {
const text = `用户ID:${this.userId} ${this.timestamp}`
const font = '14vp sans-serif'
const angle = -25 * Math.PI / 180 // 倾斜25度 // 计算文本尺寸
this.context.font = font
const { width: textWidth, height: textHeight } = this.context.measureText(text) // 坐标系变换:先平移到左下角,再旋转
this.context.save()
this.context.translate(0, this.context.height) // 原点移至左下角
this.context.rotate(angle) // 逆时针旋转25度 // 计算行列间隔(1.5倍文本尺寸避免重叠)
const colGap = textWidth * 1.5
const rowGap = textHeight * 1.5
const cols = Math.ceil(this.context.width / colGap)
const rows = Math.ceil(this.context.height / rowGap) // 绘制网格
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const x = i * colGap
const y = j * rowGap
this.context.fillText(text, x, -y) // y轴反转(因为原点在左下角)
}
} this.context.restore() // 恢复原始坐标系
}

4. 页面集成:用 Stack 实现水印层覆盖

将水印组件与笔记内容层叠加,推荐使用 Stack 布局,清晰且性能稳定:

// pages/NoteDetail.ets
@Entry
@Component
struct NoteDetail {
private noteContent = '这里是用户的笔记正文...' build() {
Stack() {
// 笔记内容层
Column()
.padding(24)
.spacing(16)
.text(this.noteContent)
.fontSize(16)
.lineHeight(24) // 水印层(覆盖在内容上方)
UserWatermark()
}
.backgroundColor(Color.White)
.width('100%')
.height('100%')
}
}

三、性能优化

  1. 离屏渲染优化

    如果遇到页面卡顿,可以尝试将 Canvas 替换为 OffscreenCanvas 进行离屏绘制,减少主线程压力:

    // 离屏画布版本(适用于复杂场景)
    private offscreenCanvas = new OffscreenCanvas()
    private offscreenContext = this.offscreenCanvas.getContext('2d')! // 在draw方法中使用offscreenContext绘制,最后同步到主画布
    this.context.drawImage(this.offscreenCanvas, 0, 0)
  2. 文本测量缓存

    重复计算文本宽度会影响性能,因此将 measureText 的结果缓存:

    private textMetrics: TextMetrics | null = null
    
    private getTextSize(text: string) {
    if (!this.textMetrics || this.textMetrics.text !== text) {
    this.textMetrics = this.context.measureText(text)
    }
    return this.textMetrics
    }
  3. 触摸事件优化

    通过 hitTestBehavior: HitTestMode.Transparent 让水印层完全透明,触摸事件直接穿透到下层内容,不影响用户操作。

四、从页面到全场景的水印方案

如果你的应用需要支持更多场景,还可以参考官方文档中的其他能力:

  • 图片水印:用 OffscreenCanvas 实现本地图片加水印(适合用户保存笔记截图时自动加水印)
  • PDF 水印:通过 pdfService 模块给导出的 PDF 文档添加水印(企业需求必备)
  • 动态变色:根据页面主题切换水印颜色(浅色/深色模式适配)

五、那些让我半夜睡不着的细节

  1. 旋转方向的坑

    坐标系默认顺时针旋转,想实现「向左倾斜」需要用负数角度(如 -25度),刚开始用正数导致水印方向搞反。

  2. 设备适配问题

    不同设备的 vp 单位换算有差异,建议统一处理尺寸问题。

  3. 性能监控

    用 DevEco Studio 的「性能调优」工具监控 onReadydraw 方法的执行时间,确保单次绘制不超过 16ms(60fps标准)。

六、总结:水印背后的安全哲学

加水印本质是一种「威慑性防护」,它不能完全阻止截图,但能大大增加内容泄露后的追溯成本。在实际开发中,建议结合以下策略:

  • 前端水印:防止非授权截图传播
  • 后端日志:记录用户操作时间线
  • 数据加密:敏感内容本地加密存储

技术之外,更重要的是平衡用户体验与安全需求——毕竟,没有人喜欢被密密麻麻的水印「包围」。通过透明度调整(建议 opacity: 0.15-0.2)和合理的排列间隔,完全可以做到「水印可见但不干扰阅读」。

如果你在开发中遇到其他有趣的场景,欢迎在评论区交流~ 一起用技术让内容安全更优雅一点~

HarmonyOS 实战:给笔记应用加防截图水印的更多相关文章

  1. Linux实战教学笔记05:远程SSH连接服务与基本排错(新手扫盲篇)

    第五节 远程SSH连接服务与基本排错 标签(空格分隔):Linux实战教学笔记-陈思齐 第1章 远程连接LInux系统管理 1.1 为什么要远程连接Linux系统 在实际的工作场景中,虚拟机界面或物理 ...

  2. Linux实战教学笔记08:Linux 文件的属性(上半部分)

    第八节 Linux 文件的属性(上半部分) 标签(空格分隔):Linux实战教学笔记 第1章 Linux中的文件 1.1 文件属性概述(ls -lhi) linux里一切皆文件 Linux系统中的文件 ...

  3. Linux实战教学笔记07:Linux系统目录结构介绍

    第七节 Linux系统目录结构介绍 标签(空格分隔):Linux实战教学笔记 第1章 前言 windows目录结构 C:\windows D:\Program Files E:\你懂的\精品 F:\你 ...

  4. Linux实战教学笔记06:Linux系统基础优化

    第六节 Linux系统基础优化 标签(空格分隔):Linux实战教学笔记-陈思齐 第1章 基础环境 第2章 使用网易163镜像做yum源 默认国外的yum源速度很慢,所以换成国内的. 第一步:先备份 ...

  5. Linux实战教学笔记04:Linux命令基础

    第四节:Linux命令基础 标签(空格分隔):Linux实战教学笔记 第1章 认识操作环境 root:当前登陆的用户名 @分隔符 chensiqi:主机名 -:当前路径位置 用户的提示符 1.1 Li ...

  6. Linux实战教学笔记03:操作系统发展历程及系统版本选择

    标签(空格分隔): Linux实战教学笔记-陈思齐 第1章 Linux简介 1.1 什么是操作系统? 简单讲:操作系统就是一个人与计算机硬件的中介. 操作系统,英文名称Operating System ...

  7. Linux实战教学笔记02:计算机系统硬件核心知识

    标签(空格分隔):Linux实战教学笔记-陈思齐 第1章 互联网企业常见服务器介绍 1.1 互联网公司服务器品牌 - DELL(大多数公司,常用) - HP - IBM(百度在用) 浪潮 联想 航天联 ...

  8. Linux实战教学笔记01:计算机硬件组成与基本原理

    标签(空格分隔): Linux实战教学笔记 第1章 如何学习Linux 要想学好任何一门学问,不仅要眼睛看,耳朵听,还要动手记,勤思考,多交流甚至尝试着去教会别人. 第2章 服务器 2.1 运维的基本 ...

  9. 机器学习实战 - 读书笔记(13) - 利用PCA来简化数据

    前言 最近在看Peter Harrington写的"机器学习实战",这是我的学习心得,这次是第13章 - 利用PCA来简化数据. 这里介绍,机器学习中的降维技术,可简化样品数据. ...

  10. 机器学习实战 - 读书笔记(12) - 使用FP-growth算法来高效发现频繁项集

    前言 最近在看Peter Harrington写的"机器学习实战",这是我的学习心得,这次是第12章 - 使用FP-growth算法来高效发现频繁项集. 基本概念 FP-growt ...

随机推荐

  1. Navicat 如何将表恢复默认状态下

    场景: 测试一套流程后,造测试数据非常麻烦的情况下,如何通过更改数据库为默认情况即初始表数据 案例: 比如表原有结构如下图(一) 修改后数据如下图(二): 需求:将图二数据恢复到图一内容下 操作思想: ...

  2. linux中如何判断一个rpm是手动安装还是通过yum安装的

    现状 对于一个不熟悉的服务器或者是虽然是自己的服务器,但历史比较久远,对于上面安装了的一些软件包,我们记忆都慢慢模糊了. 我今天遇到一个情况,在安装一个工具x2openEuler时,安装失败,提示依赖 ...

  3. MaxKB+Ollama 离线部署

    主题:在 Centos7 环境部署 MaxKB 以及 Ollama 实现基于离线大模型的的小助手调用. 选择离线部署的原因:原计划是打算直接使用 1Panel 进行 MaxKB 和 Ollama 一键 ...

  4. 征婚 SQL

    [男]程序员是这么征婚滴 SELECT * FROM 女人们  WHERE 未婚=true  AND Gay=false AND 处女=true AND 有魅力 =true AND 条件 IN (漂亮 ...

  5. IDEA中高效配置Python开发环境搭建

    原文地址:IDEA中高效配置Python开发环境搭建-张苹果博客 Mac用户须知:系统已预装Python 2.7,如需新版建议通过Homebrew安装. # 张苹果博客:https://zhangpi ...

  6. 神级辅助工具,解决GPT-SoVITS配音发音纠正和逐句优化

    即使地表最强AI配音也无法自动识别360应配音成三百六十还是三六零,在长文配音中很难一次满意,总会因为个别几句配音不理想而毁掉整个配音成果. 在GPT-SoVITS配音中,自动把长文章拆分成段落或长句 ...

  7. 🎀springboot banner介绍及使用

    简介 Banner是指应用程序启动时显示的信息.对于Spring Boot应用来说,默认情况下,当你启动一个 Spring Boot应用时,控制台上会打印出一段由 Spring Boot自带的ASCI ...

  8. 康谋分享 | 在基于场景的AD/ADAS验证过程中,识别挑战性场景!

    基于场景的验证是AD/ADAS(自动驾驶和高级驾驶辅助)系统开发过程中的重要步骤,它包括对自动化系统进行一系列预定义场景的测试.测试中包含的场景越多,尤其挑战性场景越多,人们对正在测试的AD/ADAS ...

  9. 🔥吐血整理 Bolt.diy 部署与应用攻略

    1.前言 以前总是有很多人无代码基础的人总是在幻想,如何不要自己写代码就可以建立一个自己的创意网站呢?之前总觉得异想天开不可能,屏幕前的你是不是也是这么想的呢,没有想到,Bolt.diy帮你实现了,快 ...

  10. 5.3K star!硅基生命新纪元,这个开源数字人框架要火!

    嗨,大家好,我是小华同学,关注我们获得"最新.最全.最优质"开源项目和高效工作学习方法 "只需3分钟视频素材,就能打造专属数字分身!""开源免费商用, ...