在鸿蒙 PC 与 2in1 设备应用开发中,默认的矩形窗口已无法满足多样化交互需求。异形窗口(如圆形放大镜、带箭头气泡框、自定义不规则悬浮窗)能极大提升 UI 交互的灵活性与用户体验,成为进阶开发的重要技能。本文基于华为官方开发指南,从原理解析、开发流程、场景实战到代码实现,全方位带大家掌握鸿蒙异形窗口开发,兼顾教学深度与实操性。

一、异形窗口核心原理:掩码驱动的形状定制

鸿蒙异形窗口的实现核心是窗口掩码(Window Mask) 机制,其底层逻辑可概括为 “像素级透明控制”,具体原理如下:

  1. 默认形态:ArkUI 创建窗口时,以宽高为参数生成默认矩形窗口,所有像素默认不透明;
  2. 掩码定义:掩码是一个与窗口宽高完全对应的二维数组,数组元素仅支持01——0表示对应位置像素透明,1表示不透明;
  3. 形状生效:通过setWindowMask()接口将掩码应用于窗口,系统会根据数组中1的分布,渲染出非透明区域构成的异形形状,本质是 “点阵图定义窗口轮廓”。

关键说明:掩码数组的宽高必须与窗口初始宽高完全一致,否则形状设置会失效;数组元素仅支持01,超出范围将导致渲染异常。

二、异形窗口开发通用流程(必掌握)

无论实现何种异形窗口,都需遵循 “创建基础窗口→生成掩码→应用掩码→显示窗口” 的核心流程,步骤如下:

  1. 环境配置:配置syscap.json文件,启用窗口形状控制相关系统能力;
  2. 创建基础窗口:通过createSubWindow()创建子窗口或createWindow()创建悬浮窗,设置位置、大小及加载的 UI 页面;
  3. 生成目标掩码:根据需求(几何图形 / 不规则图形),生成对应的二维数组掩码;
  4. 应用并显示:调用setWindowMask()接口绑定掩码,通过showWindow()显示异形窗口。

其中,掩码生成是差异化核心,几何图形需通过算法计算掩码,不规则图形需基于图片转化掩码。

三、场景实战 1:几何图形窗口(圆形 / 三角形)

几何图形窗口具有规则轮廓,可通过数学算法精确生成掩码,适用于放大镜、简单悬浮按钮等场景。以下是完整开发步骤与代码实现。

1. 第一步:环境配置(关键前置)

在模块src/main目录下创建syscap.json文件,声明 2in1 设备支持及窗口会话管理能力,否则setWindowMask()接口无法使用:

// entry/src/main/syscap.json
{
  "devices": {
    "general": [
      "2in1"
    ]
  },
  "development": {
    "addedSysCaps": [
      "SystemCapability.Window.SessionManager"
    ]
  }
}

2. 第二步:创建基础矩形窗口

先创建默认矩形子窗口,指定宽高、位置并绑定 UI 页面,作为异形窗口的 “载体”:

// Index.ets
import window from '@ohos.window';
import hilog from '@ohos.hilog';
import { setWindowCircleShape, setWindowTriangleShape } from './WindowUtils';

let windowStage: window.WindowStage | null = null;
let subWindow: window.Window | null = null;

// 初始化窗口舞台(通常在应用启动时获取)
AppStorage.set('windowStage', getContext().windowStage);

// 创建基础子窗口
async function createBaseSubWindow() {
  windowStage = AppStorage.get('windowStage');
  if (!windowStage) {
    hilog.error(0x0000, 'ShapeWindow', '获取windowStage失败');
    return;
  }

  const windowWidth = 500; // 窗口宽
  const windowHeight = 500; // 窗口高

  try {
    // 创建子窗口,指定窗口ID
    subWindow = await windowStage.createSubWindow('shapeSubWindow');
    // 设置窗口位置(屏幕x=300,y=300)
    subWindow.moveWindowTo(300, 300);
    // 设置窗口大小
    subWindow.resize(windowWidth, windowHeight);
    // 绑定UI页面(需提前创建SubPage.ets)
    subWindow.setUIContent('pages/SubPage');
    hilog.info(0x0000, 'ShapeWindow', '基础窗口创建成功');
  } catch (error) {
    hilog.error(0x0000, 'ShapeWindow', `创建窗口失败:${JSON.stringify(error)}`);
  }
}

3. 第三步:生成几何图形掩码(核心工具类)

创建WindowUtils.ets工具类,封装圆形、三角形的掩码生成算法及形状设置方法:

// WindowUtils.ets
import window from '@ohos.window';
import hilog from '@ohos.hilog';

/**
 * 生成圆形掩码:以窗口中心为圆心,直径=窗口最小边长
 * @param width 窗口宽
 * @param height 窗口高
 * @returns 圆形对应的二维数组掩码
 */
function fillCircle(array: number[][], centerX: number, centerY: number, radius: number): void {
  // 遍历窗口所有像素点
  for (let i = 0; i < array.length; i++) {
    for (let j = 0; j < array[0].length; j++) {
      // 计算当前像素到圆心的距离平方
      const distSquared = Math.pow(i - centerX, 2) + Math.pow(j - centerY, 2);
      // 距离≤半径则设为不透明(1),否则透明(0)
      if (distSquared <= Math.pow(radius, 2)) {
        array[i][j] = 1;
      }
    }
  }
}

export async function getCircleMask(width: number, height: number): Promise<number[][]> {
  const radius = Math.min(width, height) / 2; // 半径=最小边长/2
  // 初始化掩码数组,所有元素默认0(全透明)
  const maskArray: number[][] = new Array(height).fill(null).map(() => new Array(width).fill(0));
  fillCircle(maskArray, height / 2, width / 2, radius); // 圆心在窗口中心
  return maskArray;
}

/**
 * 生成三角形掩码:以窗口底边为底,窗口高度为高的等腰三角形
 * @param width 窗口宽(底边长度)
 * @param height 窗口高
 * @returns 三角形对应的二维数组掩码
 */
function fillTriangle(array: number[][], base: number, height: number): void {
  // 遍历窗口纵向像素(y轴)
  for (let i = 0; i < height - 10; i++) { // 减去10避免圆角
    // 遍历窗口横向像素(x轴)
    for (let j = 0; j < base; j++) {
      // 计算当前行的三角形宽度范围,在范围内则设为1
      const leftBound = base / 2 - (base / 2) * (i / height);
      const rightBound = base / 2 + (base / 2) * (i / height);
      if (j >= leftBound && j <= rightBound) {
        array[i][j] = 1;
      }
    }
  }
}

export async function getTriangleMask(width: number, height: number): Promise<number[][]> {
  const maskArray: number[][] = new Array(height).fill(null).map(() => new Array(width).fill(0));
  fillTriangle(maskArray, width, height);
  return maskArray;
}

/**
 * 通用形状设置方法
 * @param win 目标窗口实例
 * @param width 窗口宽
 * @param height 窗口高
 * @param getMaskFunc 掩码生成函数
 */
async function setWindowShape(win: window.Window, width: number, height: number, getMaskFunc: (w: number, h: number) => Promise<number[][]>) {
  const windowMask = await getMaskFunc(width, height);
  // 检查设备是否支持该系统能力
  if (canIUse('SystemCapability.Window.SessionManager')) {
    win.setWindowMask(windowMask);
    hilog.info(0x0000, 'ShapeWindow', '窗口形状设置成功');
  } else {
    hilog.warn(0x0000, 'ShapeWindow', '设备不支持异形窗口能力');
  }
}

/**
 * 对外暴露:设置窗口为圆形
 */
export async function setWindowCircleShape(win: window.Window, width: number, height: number) {
  await setWindowShape(win, width, height, getCircleMask);
}

/**
 * 对外暴露:设置窗口为三角形
 */
export async function setWindowTriangleShape(win: window.Window, width: number, height: number) {
  await setWindowShape(win, width, height, getTriangleMask);
}

4. 第四步:应用形状并显示窗口

Index.ets中调用工具类方法,为基础窗口设置形状并显示:

// Index.ets 继续添加
async function showCircleWindow() {
  await createBaseSubWindow();
  if (subWindow) {
    const width = 500;
    const height = 500;
    await setWindowCircleShape(subWindow, width, height); // 设置圆形
    subWindow.showWindow(); // 显示异形窗口
  }
}

async function showTriangleWindow() {
  await createBaseSubWindow();
  if (subWindow) {
    const width = 500;
    const height = 500;
    await setWindowTriangleShape(subWindow, width, height); // 设置三角形
    subWindow.showWindow(); // 显示异形窗口
  }
}

// 页面加载时触发(可绑定按钮点击事件)
aboutToAppear() {
  // 显示圆形窗口(按需切换为showTriangleWindow())
  showCircleWindow();
}

5. 实现效果

  • 圆形窗口:以窗口中心为圆心,直径 500 的标准圆形,透明区域隐藏,仅显示圆形内 UI 内容;
  • 三角形窗口:底边与窗口宽度一致、高与窗口高度一致的等腰三角形,顶部尖锐、底部平整。

四、场景实战 2:不规则形状窗口(基于图片)

不规则形状窗口(如品牌 Logo 形状、带箭头的气泡框)需基于设计图生成,核心是将图片转化为掩码。以下是完整实现流程。

1. 前置准备

  • 准备透明背景的 PNG 图片(如 Logo 图),放入main_pages目录下的rawfile文件夹(无该文件夹需手动创建);
  • 确保图片尺寸与窗口宽高一致,避免拉伸导致形状变形。

2. 第一步:图片转 PixelMap 工具类

创建ImageUtils.ets,封装图片读取与 PixelMap 转换能力(PixelMap 是鸿蒙图片像素级操作的核心载体):

// ImageUtils.ets
import image from '@ohos.multimedia.image';
import resourceManager from '@ohos.resourceManager';
import { Context } from '@ohos.ui';

/**
 * 将图片文件转化为PixelMap对象(像素级操作基础)
 * @param context 应用上下文
 * @param icon 图片路径(rawfile下的相对路径)
 * @param w 目标宽度(与窗口宽一致)
 * @param h 目标高度(与窗口高一致)
 * @returns PixelMap对象
 */
export async function image2PixelMap(context: Context, icon: string, w: number, h: number): Promise<image.PixelMap> {
  try {
    // 获取图片资源的文件描述符
    const rawFileDescriptor: resourceManager.RawFileDescriptor = context.resourceManager.getRawFdSync(icon);
    // 创建图片源
    const imageSource: image.ImageSource = image.createImageSource(rawFileDescriptor);
    // 生成PixelMap(指定格式为BGRA_8888,包含Alpha通道)
    const pixelMap = await imageSource.createPixelMap({
      editable: false,
      desiredPixelFormat: image.PixelMapFormat.BGRA_8888,
      desiredSize: { width: w, height: h }
    });
    // 释放图片源资源
    imageSource.release();
    return pixelMap;
  } catch (error) {
    throw new Error(`图片转PixelMap失败:${JSON.stringify(error)}`);
  }
}

3. 第二步:生成不规则图形掩码(扩展 WindowUtils)

WindowUtils.ets中添加基于图片的掩码生成方法,核心是提取图片 Alpha 通道(透明度)判断像素是否透明:

// WindowUtils.ets 新增代码
import { Context } from '@ohos.ui';
import { image2PixelMap } from './ImageUtils';

/**
 * 基于图片生成不规则形状掩码
 * @param context 应用上下文
 * @param width 窗口宽
 * @param height 窗口高
 * @param picPath 图片路径(rawfile下的相对路径)
 * @returns 不规则形状掩码
 */
export async function getPicMask(context: Context, width: number, height: number, picPath: string): Promise<number[][]> {
  // 初始化全透明掩码
  const maskArray: number[][] = new Array(height).fill(null).map(() => new Array(width).fill(0));
  // 将图片转为PixelMap
  const pixelMap = await image2PixelMap(context, picPath, width, height);
  // 创建缓冲区存储像素数据(BGRA_8888格式:每个像素占4字节,顺序为B-G-R-A)
  const pixelArrayBuffer: ArrayBuffer = new ArrayBuffer(width * height * 4);
  // 读取像素数据到缓冲区
  await pixelMap.readPixelsToBuffer(pixelArrayBuffer);
  const unit8Pixels: Uint8Array = new Uint8Array(pixelArrayBuffer);
  
  const allPixels: number[] = [];
  // 提取Alpha通道值(每个像素的第4字节,索引i+3)
  for (let i = 0, j = 0; i < unit8Pixels.length; i += 4, j++) {
    // Alpha>0表示不透明,设为1;否则设为0
    allPixels[j] = unit8Pixels[i + 3] > 0 ? 1 : 0;
  }
  
  // 释放PixelMap资源
  pixelMap.release();
  
  // 将一维像素数组转为二维掩码数组(与窗口宽高对应)
  let k = 0;
  for (let i = 0; i < height; i++) {
    for (let j = 0; j < width; j++) {
      maskArray[i][j] = allPixels[k++];
    }
  }
  
  return maskArray;
}

/**
 * 对外暴露:设置窗口为图片形状
 */
export async function setWindowPicShape(win: window.Window, context: Context, width: number, height: number, picPath: string) {
  await setWindowShape(win, width, height, (w, h) => getPicMask(context, w, h, picPath));
}

4. 第三步:显示不规则形状窗口

Index.ets中调用图片形状设置方法,实现不规则窗口显示:

// Index.ets 新增代码
import { setWindowPicShape } from './WindowUtils';

async function showIrregularWindow() {
  await createBaseSubWindow(); // 复用之前的基础窗口创建方法
  if (subWindow) {
    const width = 400; // 与图片宽度一致
    const height = 400; // 与图片高度一致
    const picPath = 'rawfile/logo.png'; // 图片在rawfile中的路径
    // 设置不规则形状并显示
    await setWindowPicShape(subWindow, getContext(), width, height, picPath);
    subWindow.showWindow();
  }
}

// 页面加载时显示不规则窗口
aboutToAppear() {
  showIrregularWindow();
}

5. 实现效果

窗口将完全呈现图片的非透明区域形状,透明背景部分会被隐藏,适用于品牌化悬浮窗、自定义交互组件等场景。

五、开发避坑与优化建议

  1. 掩码与窗口尺寸一致性:掩码数组的宽高必须与窗口resize()设置的宽高完全匹配,否则形状会错位或失效;
  2. 性能优化:大尺寸窗口(如超过 1000px)的掩码生成会占用较多资源,建议异步生成掩码,避免阻塞 UI 线程;
  3. 设备兼容性:异形窗口仅支持 PC/2in1 设备,开发时需通过canIUse()检查系统能力,非支持设备需降级为矩形窗口;
  4. 图片格式要求:不规则形状图片需为 PNG 格式(支持 Alpha 通道),JPG 格式无透明通道,无法生成正确掩码;
  5. 资源释放:PixelMap、ImageSource 等资源使用后需调用release()释放,避免内存泄漏。

六、总结与应用场景拓展

鸿蒙 PC/2in1 异形窗口通过掩码机制实现了高度灵活的形状定制,核心在于 “基础窗口 + 掩码生成” 的组合模式。几何图形窗口适用于简单交互组件(放大镜、悬浮按钮),不规则图形窗口适用于品牌化组件、个性化交互场景(如游戏角色头像窗口、功能指示气泡)。

进阶学习方向:结合分布式能力,实现多设备异形窗口协同(如 PC 端异形悬浮窗同步手机端内容);探索动态掩码生成,实现窗口形状的动画过渡效果。掌握异形窗口开发,能让你的鸿蒙 PC 应用在交互体验上更具竞争力。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐