在这里插入图片描述

开篇

在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.widthctx.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.rowSpacingthis.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 确保了旋转后第一个水印不会被画布左上角裁剪。

实用提示

  • 偏移量的计算依赖于 textWidthtextHeight,这些值在旋转前测量。如果文字方向改变或使用多行文本,需要相应调整偏移公式。
  • 双层循环的范围覆盖到 canvasHeight + textHeightcanvasWidth + 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%',与父容器尺寸一致,确保覆盖完整。
  • 如果水印需要根据页面滚动保持固定位置,可以结合Scrollposition属性调整。

注意事项

  • Stack的图层顺序:先写内容组件(下层),后写水印组件(上层),否则水印会被覆盖。
  • 使用Stack时,水印组件默认不可交互,若需水印区域响应点击(如动态更改配置),需单独设置hitTestBehavior

如果你想在不同页面复用该水印组件,可以将WatermarkComponent封装为自定义组件并通过@Prop传递参数。你通常使用哪种方式控制水印的显隐或动态更新?欢迎留言交流。

使用 Stackoverlay 将水印组件集成到页面

上一节实现了 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 叠加。

[截图: 页面叠加水印后的效果]


运行验证

  1. WatermarkComponent 放在 MainPageStack 中,运行应用。
  2. 页面应显示半透明的旋转水印文字(默认 机密文档,倾斜 -30°)。
  3. 点击页面中的“点击测试”按钮,控制台应输出日志,证明水印未阻挡点击事件。
  4. 若有需要,可调整 angletextColorrowSpacing 等参数观察水印排列变化。

预期表现:底层页面正常交互,水印覆盖渲染但不干扰触摸。

在这里插入图片描述


小结与预告

本篇完成了页面上添加水印的核心实现:
✅ 基于 Canvas 的自定义水印组件,支持旋转角度和起点偏移
✅ 通过 Stack 集成,并设置 hitTestBehavior 确保交互穿透
✅ 对比 overlay 的局限性,推荐优选 Stack 方案

下一篇将进入 图片上添加水印PDF 文档添加水印 场景。你将学到如何从文件或相机获取 pixelMap,通过 Canvas 合成水印并保存为新图片,以及使用 PDF 库在文档页面添加水印。届时,本系列的水印工具箱将覆盖所有主流场景。

系列文章所有代码可直接在 DevEco Studio 中运行,跟随实战,上线一个带完整水印功能的 App。


Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐