哈喽大家好我是你们的老朋友爱学习的小齐哥哥。上个月我投入开发了一款智能家居控制类的HarmonyOS应用其中有一个功能需要实现一个“取景框”效果——用户拍照时屏幕中央显示一个矩形框框内是相机预览画面框外是半透明的黑色遮罩引导用户将物品放在框内拍摄。听起来很简单对吧不就是画个矩形然后把中间挖空吗我最初也是这么想的。我尝试了简单的布局叠加结果要么遮罩层挡住了相机预览要么挖空区域边缘有锯齿。更麻烦的是当用户旋转手机时这个“取景框”还需要保持居中且比例不变。“这不就是个镂空效果吗”我最初不以为意。我尝试了多个方案要么性能差导致页面卡顿要么在不同分辨率设备上显示异常。产品经理还提出了新需求“我们以后可能要做圆形、心形甚至自定义形状的取景框难道要每种形状都写一套代码吗”今天我将彻底复盘并分享这次“镂空效果”实现的完整解决之旅。这不仅仅是一个UI绘制问题更是一次对HarmonyOS Canvas绘制能力的深度探索。你将掌握三种从简单到复杂的镂空实现方案从此轻松应对各种遮罩需求。一、问题场景那个“倔强”的取景框我的应用场景很明确在相机预览界面上需要显示一个居中的矩形取景框框内透明显示相机画面框外有半透明的黑色遮罩引导用户将商品放入框内拍摄。我最初尝试的方案是使用两个矩形叠加// 最初尝试的简单方案 Stack() { // 底层全屏半透明黑色遮罩 Column() .width(100%) .height(100%) .backgroundColor(rgba(0, 0, 0, 0.6)); // 上层中间挖空的矩形试图用背景色透明来实现 Column() .width(200) .height(200) .backgroundColor(Color.Transparent) .border({ width: 2, color: Color.White }); }实际现象中间的“透明”区域并没有真正透明而是显示了Stack的背景色相机预览被完全遮住了。更糟糕的是当我在真机上测试时发现这个方案在滑动页面时会出现明显的性能问题。核心需求我需要一个真正的镂空效果——不是用透明色假装挖空而是要让底层内容相机预览能够透过遮罩层显示出来。二、背景知识Canvas的合成与混合模式要解决镂空问题必须理解HarmonyOS Canvas的绘制原理和合成模式。Canvas提供了多种图形合成方式其中几种特别适合实现镂空效果。关键概念作用在镂空场景中的应用globalCompositeOperation控制新图形如何绘制到已有图形上使用xor模式可实现重叠区域透明clearRect清除指定矩形区域内的所有绘制内容直接在绘制层上“挖洞”blendMode控制控件内容与下层内容的混合方式组件级别的混合效果关键洞察globalCompositeOperation: xor异或模式当两个图形重叠时重叠部分会变成透明。这是实现镂空最直接的方式。clearRect()清除画布上的像素相当于在绘制层上直接挖出一个洞。blendMode属性在组件级别实现混合效果无需直接操作Canvas上下文。三、解决方案一XOR异或模式最直观的方案这是最符合直觉的镂空实现方式。原理很简单先绘制一个全屏的黑色矩形然后在上面再绘制一个同样颜色的矩形使用XOR模式两个矩形重叠的部分就会变成透明。实现步骤创建Canvas上下文设置离屏渲染以提高性能设置合成模式将globalCompositeOperation设为xor绘制全屏遮罩先绘制一个覆盖整个Canvas的黑色矩形绘制挖空区域在需要镂空的位置绘制第二个黑色矩形完整代码示例import { display } from kit.ArkUI; Entry Component struct ViewfinderExample { // 创建Canvas渲染上下文启用离屏渲染 private settings: RenderingContextSettings new RenderingContextSettings(true); private context: CanvasRenderingContext2D new CanvasRenderingContext2D(this.settings); // 取景框的尺寸和位置 State viewfinderWidth: number 200; State viewfinderHeight: number 200; State viewfinderX: number 0; State viewfinderY: number 0; aboutToAppear() { // 计算取景框居中位置 this.calculateViewfinderPosition(); } calculateViewfinderPosition() { // 获取屏幕尺寸 const screenWidth this.getUIContext().px2vp(display.getDefaultDisplaySync().width); const screenHeight this.getUIContext().px2vp(display.getDefaultDisplaySync().height); // 计算居中位置考虑状态栏高度 this.viewfinderX (screenWidth - this.viewfinderWidth) / 2; this.viewfinderY (screenHeight - this.viewfinderHeight) / 2 - 20; // 减去状态栏高度 } build() { Stack({ alignContent: Alignment.Center }) { // 底层相机预览这里用图片模拟 Image($r(app.media.camera_preview)) .width(100%) .height(100%) .objectFit(ImageFit.Cover); // 中层Canvas镂空遮罩 Canvas(this.context) .width(100%) .height(100%) .onReady(() { // 关键步骤1设置异或合成模式 this.context.globalCompositeOperation xor; // 关键步骤2绘制全屏黑色遮罩 this.context.beginPath(); this.context.rect(0, 0, this.getUIContext().px2vp(display.getDefaultDisplaySync().width), this.getUIContext().px2vp(display.getDefaultDisplaySync().height)); this.context.closePath(); this.context.fillStyle rgba(0, 0, 0, 0.7); // 70%不透明度的黑色 this.context.fill(); // 关键步骤3绘制取景框区域与遮罩重叠部分会变透明 this.context.beginPath(); this.context.rect(this.viewfinderX, this.viewfinderY, this.viewfinderWidth, this.viewfinderHeight); this.context.closePath(); this.context.fillStyle rgba(0, 0, 0, 0.7); this.context.fill(); }) .opacity(0.9); // 上层取景框边框和提示文字 Column() { // 取景框边框 Column() .width(this.viewfinderWidth) .height(this.viewfinderHeight) .border({ width: 2, color: Color.White }) .shadow({ radius: 10, color: #1AFFFFFF }); // 提示文字 Text(请将物品放入框内) .fontSize(14) .fontColor(Color.White) .margin({ top: 20 }) .backgroundColor(rgba(0, 0, 0, 0.5)) .padding(8) .borderRadius(4); } } .width(100%) .height(100%) .backgroundColor(Color.Black); } }方案优势与局限✅ 优势实现简单直观代码量少性能较好适合静态或低频更新的场景透明度可调视觉效果灵活⚠️ 局限XOR模式对颜色敏感必须使用相同的颜色值不支持渐变或图片作为遮罩在动态变化场景中需要频繁重绘四、解决方案二clearRect清除区域最灵活的方案如果你需要更精细的控制或者要在已经绘制了复杂图形的画布上挖洞clearRect()是更好的选择。这个方法直接在像素级别清除指定区域相当于在画布上挖出一个真正的洞。实现原理正常绘制背景先绘制遮罩层或其他图形清除指定区域在需要镂空的位置调用clearRect()可选重新绘制边框在清除的区域周围绘制边框线进阶示例动态多形状镂空Entry Component struct DynamicViewfinder { private settings: RenderingContextSettings new RenderingContextSettings(true); private context: CanvasRenderingContext2D new CanvasRenderingContext2D(this.settings); // 支持多种形状的取景框 State viewfinderType: rectangle | circle | rounded rectangle; State viewfinderSize: number 180; // 动画状态 State isScanning: boolean false; State scanLineY: number 0; build() { Column() { // 形状选择器 Row({ space: 15 }) { Button(矩形) .onClick(() { this.viewfinderType rectangle; }) .backgroundColor(this.viewfinderType rectangle ? #007DFF : #F0F0F0); Button(圆形) .onClick(() { this.viewfinderType circle; }) .backgroundColor(this.viewfinderType circle ? #007DFF : #F0F0F0); Button(圆角) .onClick(() { this.viewfinderType rounded; }) .backgroundColor(this.viewfinderType rounded ? #007DFF : #F0F0F0); } .padding(20) .width(100%) .justifyContent(FlexAlign.Center); // Canvas绘制区域 Canvas(this.context) .width(300) .height(300) .margin({ top: 30 }) .backgroundColor(#1A1A1A) .onReady(() { this.drawViewfinder(); }) .onClick(() { // 点击开始/停止扫描动画 this.isScanning !this.isScanning; if (this.isScanning) { this.startScanAnimation(); } }); // 操作提示 Text(this.isScanning ? 点击停止扫描 : 点击开始扫描) .fontSize(12) .fontColor(#999999) .margin({ top: 10 }); } .width(100%) .height(100%) .backgroundColor(#F5F5F5) .alignItems(HorizontalAlign.Center); } drawViewfinder() { const centerX 150; const centerY 150; const radius this.viewfinderSize / 2; // 1. 绘制半透明黑色背景 this.context.fillStyle rgba(0, 0, 0, 0.7); this.context.fillRect(0, 0, 300, 300); // 2. 根据选择的形状清除相应区域 this.context.save(); switch (this.viewfinderType) { case rectangle: // 矩形镂空 this.context.clearRect( centerX - radius, centerY - radius, this.viewfinderSize, this.viewfinderSize ); // 绘制矩形边框 this.context.strokeStyle #34C759; this.context.lineWidth 3; this.context.strokeRect( centerX - radius, centerY - radius, this.viewfinderSize, this.viewfinderSize ); break; case circle: // 圆形镂空 this.context.beginPath(); this.context.arc(centerX, centerY, radius, 0, Math.PI * 2); this.context.clip(); // 设置裁剪区域 this.context.clearRect(0, 0, 300, 300); // 绘制圆形边框 this.context.strokeStyle #FF9500; this.context.lineWidth 3; this.context.beginPath(); this.context.arc(centerX, centerY, radius, 0, Math.PI * 2); this.context.stroke(); break; case rounded: // 圆角矩形镂空 const cornerRadius 20; this.context.beginPath(); this.context.roundRect( centerX - radius, centerY - radius, this.viewfinderSize, this.viewfinderSize, cornerRadius ); this.context.clip(); this.context.clearRect(0, 0, 300, 300); // 绘制圆角矩形边框 this.context.strokeStyle #AF52DE; this.context.lineWidth 3; this.context.beginPath(); this.context.roundRect( centerX - radius, centerY - radius, this.viewfinderSize, this.viewfinderSize, cornerRadius ); this.context.stroke(); break; } this.context.restore(); // 3. 如果正在扫描绘制扫描线 if (this.isScanning) { this.drawScanLine(); } } drawScanLine() { this.context.strokeStyle #FF3B30; this.context.lineWidth 2; this.context.setLineDash([5, 3]); // 虚线样式 this.context.beginPath(); this.context.moveTo(150 - this.viewfinderSize / 2, this.scanLineY); this.context.lineTo(150 this.viewfinderSize / 2, this.scanLineY); this.context.stroke(); this.context.setLineDash([]); // 恢复实线 } startScanAnimation() { const startY 150 - this.viewfinderSize / 2; const endY 150 this.viewfinderSize / 2; this.scanLineY startY; const animate () { if (!this.isScanning) return; this.scanLineY 2; if (this.scanLineY endY) { this.scanLineY startY; } // 重绘Canvas this.drawViewfinder(); // 继续下一帧动画 setTimeout(animate, 16); // 约60fps }; animate(); } }方案优势与局限✅ 优势支持任意形状的镂空通过clip()方法可以在复杂图形上挖洞性能优秀浏览器原生支持⚠️ 局限清除的区域完全透明无法做半透明效果需要手动管理绘制顺序清除后无法恢复原有像素五、解决方案三blendMode混合模式组件化方案如果你不想直接操作Canvas或者需要将镂空效果集成到现有的组件体系中可以使用HarmonyOS的blendMode属性。这是最“声明式”的实现方式。实现原理利用BlendMode.XOR混合模式让两个重叠组件的重叠部分产生透明效果。实用示例拍照引导界面Entry Component struct CameraGuide { State flashLightOn: boolean false; State showGrid: boolean true; build() { Stack() { // 第1层相机预览背景 Column() { Image($r(app.media.camera_preview)) .width(100%) .height(100%) .objectFit(ImageFit.Cover); } .width(100%) .height(100%); // 第2层网格辅助线可选 if (this.showGrid) { this.buildGridOverlay(); } // 第3层镂空遮罩层 Stack() { // 全屏黑色遮罩 Column() .width(100%) .height(100%) .backgroundColor(rgba(0, 0, 0, 0.65)); // 圆形取景框 - 使用XOR混合模式 Circle({ width: 220, height: 220 }) .blendMode(BlendMode.XOR, BlendApplyType.OFFSCREEN) // 关键代码 .margin({ top: 100 }); } .blendMode(BlendMode.SRC_OVER, BlendApplyType.OFFSCREEN) .width(100%) .height(100%); // 第4层UI控制层 this.buildControlPanel(); } .width(100%) .height(100%) .backgroundColor(Color.Black); } buildGridOverlay() { const gridColor rgba(255, 255, 255, 0.3); return Canvas(new CanvasRenderingContext2D(new RenderingContextSettings(true))) .width(100%) .height(100%) .onReady((ctx: CanvasRenderingContext2D) { const width 300; const height 300; const cellSize width / 3; ctx.strokeStyle gridColor; ctx.lineWidth 1; // 绘制垂直线 for (let i 1; i 3; i) { ctx.beginPath(); ctx.moveTo(i * cellSize, 0); ctx.lineTo(i * cellSize, height); ctx.stroke(); } // 绘制水平线 for (let i 1; i 3; i) { ctx.beginPath(); ctx.moveTo(0, i * cellSize); ctx.lineTo(width, i * cellSize); ctx.stroke(); } }) .opacity(0.6); } buildControlPanel() { return Column({ space: 20 }) { // 顶部状态栏 Row() { Text(智能扫描) .fontSize(16) .fontColor(Color.White) .fontWeight(FontWeight.Medium); Blank(); // 闪光灯按钮 Button(this.flashLightOn ? : ⚡) .fontSize(18) .backgroundColor(rgba(0, 0, 0, 0.3)) .onClick(() { this.flashLightOn !this.flashLightOn; }); // 网格开关 Button(this.showGrid ? 网格开 : 网格关) .fontSize(12) .backgroundColor(rgba(0, 0, 0, 0.3)) .fontColor(Color.White) .onClick(() { this.showGrid !this.showGrid; }); } .width(100%) .padding({ left: 20, right: 20, top: 50 }); Blank(); // 底部操作栏 Row({ space: 40 }) { // 相册按钮 Button() .fontSize(24) .backgroundColor(rgba(255, 255, 255, 0.1)) .width(50) .height(50) .borderRadius(25); // 拍照按钮 Button(⭕) .fontSize(36) .backgroundColor(Color.White) .width(70) .height(70) .borderRadius(35) .shadow({ radius: 10, color: #40000000 }); // 滤镜按钮 Button() .fontSize(24) .backgroundColor(rgba(255, 255, 255, 0.1)) .width(50) .height(50) .borderRadius(25); } .width(100%) .justifyContent(FlexAlign.Center) .padding({ bottom: 40 }); } .width(100%) .height(100%); } }方案优势与局限✅ 优势声明式API易于理解和使用与ArkUI组件体系完美集成支持硬件加速性能优秀⚠️ 局限混合模式种类有限效果不如Canvas灵活某些混合模式在不同设备上可能有差异无法实现复杂的多层混合效果六、方案对比与选型建议方案适用场景性能灵活性上手难度XOR异或模式简单矩形/圆形镂空静态或低频更新⭐⭐⭐⭐⭐⭐⭐⭐⭐clearRect清除复杂形状、动态镂空、需要精细控制⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐blendMode混合组件化开发、简单遮罩、快速原型⭐⭐⭐⭐⭐⭐⭐实战选型指南简单引导层使用blendMode方案快速实现且性能好证件照取景框使用clearRect方案支持圆角矩形等复杂形状AR测量工具使用XOR方案需要实时更新且形状简单创意相机滤镜组合使用多种方案不同区域用不同效果七、核心要点总结回顾这次开发经历我从“怎么画个框”的简单需求深入探索了HarmonyOS Canvas绘制的三种核心镂空技术。以下是实战中的关键收获理解合成原理Canvas的globalCompositeOperation不是魔法而是基于颜色通道的数学运算。XOR模式要求两个图形颜色相同才能产生透明效果。掌握清除技巧clearRect()是真正的像素清除配合clip()可以实现任意形状的镂空。但要注意清除后无法撤销。善用混合模式blendMode属性让镂空效果组件化适合集成到现有UI体系中。但效果相对固定不够灵活。性能优化要点对于静态遮罩使用离屏渲染RenderingContextSettings(true)对于动态效果控制重绘频率避免每帧都重绘整个Canvas复杂场景考虑分层绘制将静态和动态内容分离多设备适配不同分辨率的设备需要动态计算取景框位置和大小使用display.getDefaultDisplaySync()获取屏幕信息。镂空效果不只是视觉装饰更是用户体验的关键环节。 通过本实战方案你不仅能实现各种酷炫的遮罩效果更能深入理解HarmonyOS图形绘制的底层原理。希望这篇深度解析能助你在HarmonyOS应用开发中轻松驾驭Canvas绘制创造更出色的用户体验。
HarmonyOS 6实战5:Canvas实现镂空效果
哈喽大家好我是你们的老朋友爱学习的小齐哥哥。上个月我投入开发了一款智能家居控制类的HarmonyOS应用其中有一个功能需要实现一个“取景框”效果——用户拍照时屏幕中央显示一个矩形框框内是相机预览画面框外是半透明的黑色遮罩引导用户将物品放在框内拍摄。听起来很简单对吧不就是画个矩形然后把中间挖空吗我最初也是这么想的。我尝试了简单的布局叠加结果要么遮罩层挡住了相机预览要么挖空区域边缘有锯齿。更麻烦的是当用户旋转手机时这个“取景框”还需要保持居中且比例不变。“这不就是个镂空效果吗”我最初不以为意。我尝试了多个方案要么性能差导致页面卡顿要么在不同分辨率设备上显示异常。产品经理还提出了新需求“我们以后可能要做圆形、心形甚至自定义形状的取景框难道要每种形状都写一套代码吗”今天我将彻底复盘并分享这次“镂空效果”实现的完整解决之旅。这不仅仅是一个UI绘制问题更是一次对HarmonyOS Canvas绘制能力的深度探索。你将掌握三种从简单到复杂的镂空实现方案从此轻松应对各种遮罩需求。一、问题场景那个“倔强”的取景框我的应用场景很明确在相机预览界面上需要显示一个居中的矩形取景框框内透明显示相机画面框外有半透明的黑色遮罩引导用户将商品放入框内拍摄。我最初尝试的方案是使用两个矩形叠加// 最初尝试的简单方案 Stack() { // 底层全屏半透明黑色遮罩 Column() .width(100%) .height(100%) .backgroundColor(rgba(0, 0, 0, 0.6)); // 上层中间挖空的矩形试图用背景色透明来实现 Column() .width(200) .height(200) .backgroundColor(Color.Transparent) .border({ width: 2, color: Color.White }); }实际现象中间的“透明”区域并没有真正透明而是显示了Stack的背景色相机预览被完全遮住了。更糟糕的是当我在真机上测试时发现这个方案在滑动页面时会出现明显的性能问题。核心需求我需要一个真正的镂空效果——不是用透明色假装挖空而是要让底层内容相机预览能够透过遮罩层显示出来。二、背景知识Canvas的合成与混合模式要解决镂空问题必须理解HarmonyOS Canvas的绘制原理和合成模式。Canvas提供了多种图形合成方式其中几种特别适合实现镂空效果。关键概念作用在镂空场景中的应用globalCompositeOperation控制新图形如何绘制到已有图形上使用xor模式可实现重叠区域透明clearRect清除指定矩形区域内的所有绘制内容直接在绘制层上“挖洞”blendMode控制控件内容与下层内容的混合方式组件级别的混合效果关键洞察globalCompositeOperation: xor异或模式当两个图形重叠时重叠部分会变成透明。这是实现镂空最直接的方式。clearRect()清除画布上的像素相当于在绘制层上直接挖出一个洞。blendMode属性在组件级别实现混合效果无需直接操作Canvas上下文。三、解决方案一XOR异或模式最直观的方案这是最符合直觉的镂空实现方式。原理很简单先绘制一个全屏的黑色矩形然后在上面再绘制一个同样颜色的矩形使用XOR模式两个矩形重叠的部分就会变成透明。实现步骤创建Canvas上下文设置离屏渲染以提高性能设置合成模式将globalCompositeOperation设为xor绘制全屏遮罩先绘制一个覆盖整个Canvas的黑色矩形绘制挖空区域在需要镂空的位置绘制第二个黑色矩形完整代码示例import { display } from kit.ArkUI; Entry Component struct ViewfinderExample { // 创建Canvas渲染上下文启用离屏渲染 private settings: RenderingContextSettings new RenderingContextSettings(true); private context: CanvasRenderingContext2D new CanvasRenderingContext2D(this.settings); // 取景框的尺寸和位置 State viewfinderWidth: number 200; State viewfinderHeight: number 200; State viewfinderX: number 0; State viewfinderY: number 0; aboutToAppear() { // 计算取景框居中位置 this.calculateViewfinderPosition(); } calculateViewfinderPosition() { // 获取屏幕尺寸 const screenWidth this.getUIContext().px2vp(display.getDefaultDisplaySync().width); const screenHeight this.getUIContext().px2vp(display.getDefaultDisplaySync().height); // 计算居中位置考虑状态栏高度 this.viewfinderX (screenWidth - this.viewfinderWidth) / 2; this.viewfinderY (screenHeight - this.viewfinderHeight) / 2 - 20; // 减去状态栏高度 } build() { Stack({ alignContent: Alignment.Center }) { // 底层相机预览这里用图片模拟 Image($r(app.media.camera_preview)) .width(100%) .height(100%) .objectFit(ImageFit.Cover); // 中层Canvas镂空遮罩 Canvas(this.context) .width(100%) .height(100%) .onReady(() { // 关键步骤1设置异或合成模式 this.context.globalCompositeOperation xor; // 关键步骤2绘制全屏黑色遮罩 this.context.beginPath(); this.context.rect(0, 0, this.getUIContext().px2vp(display.getDefaultDisplaySync().width), this.getUIContext().px2vp(display.getDefaultDisplaySync().height)); this.context.closePath(); this.context.fillStyle rgba(0, 0, 0, 0.7); // 70%不透明度的黑色 this.context.fill(); // 关键步骤3绘制取景框区域与遮罩重叠部分会变透明 this.context.beginPath(); this.context.rect(this.viewfinderX, this.viewfinderY, this.viewfinderWidth, this.viewfinderHeight); this.context.closePath(); this.context.fillStyle rgba(0, 0, 0, 0.7); this.context.fill(); }) .opacity(0.9); // 上层取景框边框和提示文字 Column() { // 取景框边框 Column() .width(this.viewfinderWidth) .height(this.viewfinderHeight) .border({ width: 2, color: Color.White }) .shadow({ radius: 10, color: #1AFFFFFF }); // 提示文字 Text(请将物品放入框内) .fontSize(14) .fontColor(Color.White) .margin({ top: 20 }) .backgroundColor(rgba(0, 0, 0, 0.5)) .padding(8) .borderRadius(4); } } .width(100%) .height(100%) .backgroundColor(Color.Black); } }方案优势与局限✅ 优势实现简单直观代码量少性能较好适合静态或低频更新的场景透明度可调视觉效果灵活⚠️ 局限XOR模式对颜色敏感必须使用相同的颜色值不支持渐变或图片作为遮罩在动态变化场景中需要频繁重绘四、解决方案二clearRect清除区域最灵活的方案如果你需要更精细的控制或者要在已经绘制了复杂图形的画布上挖洞clearRect()是更好的选择。这个方法直接在像素级别清除指定区域相当于在画布上挖出一个真正的洞。实现原理正常绘制背景先绘制遮罩层或其他图形清除指定区域在需要镂空的位置调用clearRect()可选重新绘制边框在清除的区域周围绘制边框线进阶示例动态多形状镂空Entry Component struct DynamicViewfinder { private settings: RenderingContextSettings new RenderingContextSettings(true); private context: CanvasRenderingContext2D new CanvasRenderingContext2D(this.settings); // 支持多种形状的取景框 State viewfinderType: rectangle | circle | rounded rectangle; State viewfinderSize: number 180; // 动画状态 State isScanning: boolean false; State scanLineY: number 0; build() { Column() { // 形状选择器 Row({ space: 15 }) { Button(矩形) .onClick(() { this.viewfinderType rectangle; }) .backgroundColor(this.viewfinderType rectangle ? #007DFF : #F0F0F0); Button(圆形) .onClick(() { this.viewfinderType circle; }) .backgroundColor(this.viewfinderType circle ? #007DFF : #F0F0F0); Button(圆角) .onClick(() { this.viewfinderType rounded; }) .backgroundColor(this.viewfinderType rounded ? #007DFF : #F0F0F0); } .padding(20) .width(100%) .justifyContent(FlexAlign.Center); // Canvas绘制区域 Canvas(this.context) .width(300) .height(300) .margin({ top: 30 }) .backgroundColor(#1A1A1A) .onReady(() { this.drawViewfinder(); }) .onClick(() { // 点击开始/停止扫描动画 this.isScanning !this.isScanning; if (this.isScanning) { this.startScanAnimation(); } }); // 操作提示 Text(this.isScanning ? 点击停止扫描 : 点击开始扫描) .fontSize(12) .fontColor(#999999) .margin({ top: 10 }); } .width(100%) .height(100%) .backgroundColor(#F5F5F5) .alignItems(HorizontalAlign.Center); } drawViewfinder() { const centerX 150; const centerY 150; const radius this.viewfinderSize / 2; // 1. 绘制半透明黑色背景 this.context.fillStyle rgba(0, 0, 0, 0.7); this.context.fillRect(0, 0, 300, 300); // 2. 根据选择的形状清除相应区域 this.context.save(); switch (this.viewfinderType) { case rectangle: // 矩形镂空 this.context.clearRect( centerX - radius, centerY - radius, this.viewfinderSize, this.viewfinderSize ); // 绘制矩形边框 this.context.strokeStyle #34C759; this.context.lineWidth 3; this.context.strokeRect( centerX - radius, centerY - radius, this.viewfinderSize, this.viewfinderSize ); break; case circle: // 圆形镂空 this.context.beginPath(); this.context.arc(centerX, centerY, radius, 0, Math.PI * 2); this.context.clip(); // 设置裁剪区域 this.context.clearRect(0, 0, 300, 300); // 绘制圆形边框 this.context.strokeStyle #FF9500; this.context.lineWidth 3; this.context.beginPath(); this.context.arc(centerX, centerY, radius, 0, Math.PI * 2); this.context.stroke(); break; case rounded: // 圆角矩形镂空 const cornerRadius 20; this.context.beginPath(); this.context.roundRect( centerX - radius, centerY - radius, this.viewfinderSize, this.viewfinderSize, cornerRadius ); this.context.clip(); this.context.clearRect(0, 0, 300, 300); // 绘制圆角矩形边框 this.context.strokeStyle #AF52DE; this.context.lineWidth 3; this.context.beginPath(); this.context.roundRect( centerX - radius, centerY - radius, this.viewfinderSize, this.viewfinderSize, cornerRadius ); this.context.stroke(); break; } this.context.restore(); // 3. 如果正在扫描绘制扫描线 if (this.isScanning) { this.drawScanLine(); } } drawScanLine() { this.context.strokeStyle #FF3B30; this.context.lineWidth 2; this.context.setLineDash([5, 3]); // 虚线样式 this.context.beginPath(); this.context.moveTo(150 - this.viewfinderSize / 2, this.scanLineY); this.context.lineTo(150 this.viewfinderSize / 2, this.scanLineY); this.context.stroke(); this.context.setLineDash([]); // 恢复实线 } startScanAnimation() { const startY 150 - this.viewfinderSize / 2; const endY 150 this.viewfinderSize / 2; this.scanLineY startY; const animate () { if (!this.isScanning) return; this.scanLineY 2; if (this.scanLineY endY) { this.scanLineY startY; } // 重绘Canvas this.drawViewfinder(); // 继续下一帧动画 setTimeout(animate, 16); // 约60fps }; animate(); } }方案优势与局限✅ 优势支持任意形状的镂空通过clip()方法可以在复杂图形上挖洞性能优秀浏览器原生支持⚠️ 局限清除的区域完全透明无法做半透明效果需要手动管理绘制顺序清除后无法恢复原有像素五、解决方案三blendMode混合模式组件化方案如果你不想直接操作Canvas或者需要将镂空效果集成到现有的组件体系中可以使用HarmonyOS的blendMode属性。这是最“声明式”的实现方式。实现原理利用BlendMode.XOR混合模式让两个重叠组件的重叠部分产生透明效果。实用示例拍照引导界面Entry Component struct CameraGuide { State flashLightOn: boolean false; State showGrid: boolean true; build() { Stack() { // 第1层相机预览背景 Column() { Image($r(app.media.camera_preview)) .width(100%) .height(100%) .objectFit(ImageFit.Cover); } .width(100%) .height(100%); // 第2层网格辅助线可选 if (this.showGrid) { this.buildGridOverlay(); } // 第3层镂空遮罩层 Stack() { // 全屏黑色遮罩 Column() .width(100%) .height(100%) .backgroundColor(rgba(0, 0, 0, 0.65)); // 圆形取景框 - 使用XOR混合模式 Circle({ width: 220, height: 220 }) .blendMode(BlendMode.XOR, BlendApplyType.OFFSCREEN) // 关键代码 .margin({ top: 100 }); } .blendMode(BlendMode.SRC_OVER, BlendApplyType.OFFSCREEN) .width(100%) .height(100%); // 第4层UI控制层 this.buildControlPanel(); } .width(100%) .height(100%) .backgroundColor(Color.Black); } buildGridOverlay() { const gridColor rgba(255, 255, 255, 0.3); return Canvas(new CanvasRenderingContext2D(new RenderingContextSettings(true))) .width(100%) .height(100%) .onReady((ctx: CanvasRenderingContext2D) { const width 300; const height 300; const cellSize width / 3; ctx.strokeStyle gridColor; ctx.lineWidth 1; // 绘制垂直线 for (let i 1; i 3; i) { ctx.beginPath(); ctx.moveTo(i * cellSize, 0); ctx.lineTo(i * cellSize, height); ctx.stroke(); } // 绘制水平线 for (let i 1; i 3; i) { ctx.beginPath(); ctx.moveTo(0, i * cellSize); ctx.lineTo(width, i * cellSize); ctx.stroke(); } }) .opacity(0.6); } buildControlPanel() { return Column({ space: 20 }) { // 顶部状态栏 Row() { Text(智能扫描) .fontSize(16) .fontColor(Color.White) .fontWeight(FontWeight.Medium); Blank(); // 闪光灯按钮 Button(this.flashLightOn ? : ⚡) .fontSize(18) .backgroundColor(rgba(0, 0, 0, 0.3)) .onClick(() { this.flashLightOn !this.flashLightOn; }); // 网格开关 Button(this.showGrid ? 网格开 : 网格关) .fontSize(12) .backgroundColor(rgba(0, 0, 0, 0.3)) .fontColor(Color.White) .onClick(() { this.showGrid !this.showGrid; }); } .width(100%) .padding({ left: 20, right: 20, top: 50 }); Blank(); // 底部操作栏 Row({ space: 40 }) { // 相册按钮 Button() .fontSize(24) .backgroundColor(rgba(255, 255, 255, 0.1)) .width(50) .height(50) .borderRadius(25); // 拍照按钮 Button(⭕) .fontSize(36) .backgroundColor(Color.White) .width(70) .height(70) .borderRadius(35) .shadow({ radius: 10, color: #40000000 }); // 滤镜按钮 Button() .fontSize(24) .backgroundColor(rgba(255, 255, 255, 0.1)) .width(50) .height(50) .borderRadius(25); } .width(100%) .justifyContent(FlexAlign.Center) .padding({ bottom: 40 }); } .width(100%) .height(100%); } }方案优势与局限✅ 优势声明式API易于理解和使用与ArkUI组件体系完美集成支持硬件加速性能优秀⚠️ 局限混合模式种类有限效果不如Canvas灵活某些混合模式在不同设备上可能有差异无法实现复杂的多层混合效果六、方案对比与选型建议方案适用场景性能灵活性上手难度XOR异或模式简单矩形/圆形镂空静态或低频更新⭐⭐⭐⭐⭐⭐⭐⭐⭐clearRect清除复杂形状、动态镂空、需要精细控制⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐blendMode混合组件化开发、简单遮罩、快速原型⭐⭐⭐⭐⭐⭐⭐实战选型指南简单引导层使用blendMode方案快速实现且性能好证件照取景框使用clearRect方案支持圆角矩形等复杂形状AR测量工具使用XOR方案需要实时更新且形状简单创意相机滤镜组合使用多种方案不同区域用不同效果七、核心要点总结回顾这次开发经历我从“怎么画个框”的简单需求深入探索了HarmonyOS Canvas绘制的三种核心镂空技术。以下是实战中的关键收获理解合成原理Canvas的globalCompositeOperation不是魔法而是基于颜色通道的数学运算。XOR模式要求两个图形颜色相同才能产生透明效果。掌握清除技巧clearRect()是真正的像素清除配合clip()可以实现任意形状的镂空。但要注意清除后无法撤销。善用混合模式blendMode属性让镂空效果组件化适合集成到现有UI体系中。但效果相对固定不够灵活。性能优化要点对于静态遮罩使用离屏渲染RenderingContextSettings(true)对于动态效果控制重绘频率避免每帧都重绘整个Canvas复杂场景考虑分层绘制将静态和动态内容分离多设备适配不同分辨率的设备需要动态计算取景框位置和大小使用display.getDefaultDisplaySync()获取屏幕信息。镂空效果不只是视觉装饰更是用户体验的关键环节。 通过本实战方案你不仅能实现各种酷炫的遮罩效果更能深入理解HarmonyOS图形绘制的底层原理。希望这篇深度解析能助你在HarmonyOS应用开发中轻松驾驭Canvas绘制创造更出色的用户体验。