鸿蒙5.0 APP开发案例分析:PC/2in1异形窗口开发实践

往期推文全新看点(文中附带全新鸿蒙5.0全栈学习笔录)

✏️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?

✏️ 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~

✏️ 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?

✏️ 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?

✏️ 记录一场鸿蒙开发岗位面试经历~

✏️ 持续更新中……


概述

一般情况下,UI开发都是在矩形窗口上操作,但在PC/2in1设备上,应用还可以使用各类异形窗口来展现交互内容。这里的异形是指除默认矩形以外的形状,包括但不限于圆形、三角形、以及其他不规则的形状等。开发者通过使用这些异形形状的窗口,结合窗口中的内容展示UI交互的多样性,一定程度上能够提升应用体验。类似的示例有:文字应用中的放大镜、带指示箭头的气泡框、提供辅助功能的悬浮窗等。

本文主要介绍在PC/2in1设备上实现异形窗口。根据窗口的形状类别,可分为以下场景:

  • 图形形状窗口
  • 不规则形状窗口

实现原理

在创建窗口时,ArkUI以宽、高为参数描绘窗口初始大小,其形状默认为矩形,可以通过设置掩码改变窗口最终呈现的形状。

该掩码是一个二维数组,其大小对应初始窗口的宽和高,所以可以把该数组中的元素与窗口中的像素一一对应起来;数组中的每一项值只能是0或1,其中数字0表示窗口对应位置的像素是透明的,数字1代表对应像素不透明。

简单来说,掩码即是由数字1描绘的窗口形状点阵图。

开发流程

  1. 创建异形子窗口或悬浮窗,设置窗口的位置、大小等属性,设置窗口加载的page页面。
  2. 根据待实现的异形形状,计算出与其对应的二维数组掩码,对目标窗口设置该掩码。
  3. 根据逻辑需要,显示目标异形窗口。

说明
掩码windowMask二维数组的大小需要与窗口初始宽、高一致,且每项值只能是0或1,否则设置的形状不会生效。

图形形状窗口

场景案例

待开发的异形窗口,其形状是具有一定规则的几何图形,通过描述语言能得到精确且唯一的形状。本节以下面两种形状的实现为例进行说明:直径为500的圆形;底500、高500的等腰三角形。

创建子窗口

在得到异形窗口前,需要先创建一个默认的矩形窗口,开发步骤如下:

  1. 为使用setWindowMask()等接口,需要额外配置syscap.json文件,在模块/src/main目录下,创建syscap.json文件,填入以下内容:
// entry/src/main/syscap.json
{
  "devices": {
    "general": [
      "2in1"
    ]
  },
  "development": {
    "addedSysCaps": [
      "SystemCapability.Window.SessionManager"
    ]
  }
}
  1. 具体代码中,使用createSubWindow()创建子窗口,或使用createWindow()接口创建悬浮窗,设置窗口的位置、大小及其他属性等,然后使用setUIContent()接口设置窗口加载的page页面。
// entry/src/main/ets/pages/Index.ets
windowStage = AppStorage.get('windowStage');
if (windowStage === null) {
  hilog.error(0x0000, 'Sample', 'Failed to create the subwindow. Cause: windowStage is null');
  return;
}
const windowWidth = 500;
const windowHeight = 500;
try {
  subWindow = await windowStage!.createSubWindow('mySubWindow');
  subWindow.moveWindowTo(300, 300);
  subWindow.resize(windowWidth, windowHeight);
  subWindow.setUIContent('pages/SubPage');
  // ...
} catch (exception) {
  hilog.error(0x0000, 'Sample', 'Failed to create sub window. Cause: ' + JSON.stringify(exception));
}

设置窗口形状

  1. 根据要实现的异形窗口形状,计算二维数组掩码windowMask。
    • 以下是以原矩形中点为圆心、直径为500的圆。最终效果见圆形子窗口效果。
// entry/src/main/ets/utils/WindowUtils.ets
function fillCircle(array: number[][], x: number, y: number, radius: number): void {
  for (let i = x - radius; i <= x + radius; i++) {
    for (let j = y - radius; j <= y + radius; j++) {
      const distSquared = (i - x)**2 + (j - y)**2;
      if (distSquared <= radius**2 && i >= 0 && j >= 0 && i < array.length && j < array[0].length) {
        array[i][j] = 1;
      }
    }
  }
}

async function getCircleMask(width: number, height: number): Promise<number[][]> {
  const radius = Math.min(width, height) / 2;
  const maskArray: number[][] = new Array(height).fill(null).map(() => new Array(width).fill(0));
  fillCircle(maskArray, height / 2, width / 2, radius);
  return maskArray;
}
  • 以下以原矩形宽为底边、原矩形高为高的等腰三角形。最终效果见三角形子窗口效果。
// entry/src/main/ets/utils/WindowUtils.ets
function fillTriangle(array: number[][], base: number, height: number): void {
  // i < height-10 in order to remove the round corner
  for (let i = 0; i < height - 10; i++) {
    for (let j = 0; j < base; j++) {
      if (j >= base / 2 - base / 2 * i / height && j <= base / 2 + base / 2 * i / height) {
        array[i][j] = 1;
      }
    }
  }
}

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;
}
  1. 使用setWindowMask(windowMask)接口,设置窗口形状。
// entry/src/main/ets/utils/WindowUtils.ets
async function setWindowShape(win: window.Window, width: number,
  height: number, getMaskFunc: (w: number, h: number, picPath?: string) => Promise<number[][]>,
  picPath?: string): Promise<void> {
  const windowMask = await getMaskFunc(width, height, picPath);
  if (canIUse('SystemCapability.Window.SessionManager')) {
    win.setWindowMask(windowMask);
  } else {
    hilog.info(0x0000, 'Simple', 'can not use SessionManager syscap');
  }
}

/**
 * Set the window with circle shape
 * @param win The target window
 * @param width The original window width
 * @param height The original window height
 */
export async function setWindowCircleShape(win: window.Window, width: number,
  height: number): Promise<void> {
  setWindowShape(win, width, height, getCircleMask)
}

/**
 * Set the window with triangle shape
 * @param win The target window
 * @param width The original window width
 * @param height The original window height
 */
export async function setWindowTriangleShape(win: window.Window, width: number,
  height: number): Promise<void> {
  setWindowShape(win, width, height, getTriangleMask)
}
  1. 使用showWindow()接口显示异形子窗口。
// entry/src/main/ets/pages/Index.ets
subWindow.showWindow();

实现效果

图1 圆形子窗口效果

图2 三角形子窗口效果

不规则形状窗口

场景案例

待开发的异形窗口,呈现出不规则的形状。这种情况通常先由设计人员提供图形文件,开发人员以文件为输入,基于ArkUI提供的图片处理能力,转化为相应的形状掩码,最后实现不规则的窗口形状。

图3 不规则示例图

本节以上图为例说明不规则形状窗口的实现过程。

开发步骤

首先同样需要先 创建子窗口,然后再依以下步骤开发。

  1. 读取图片文件,将其转化为pixelMap数据。
// entry/src/main/ets/utils/ImageUtils.ets
/**
 * Convert image to PixelMap object
 */
export async function image2PixelMap(icon: string, w: number, h: number): Promise<image.PixelMap> {
  const rawFileDescriptor: resourceManager.RawFileDescriptor = resourceMgs.getRawFdSync(icon);
  const imageSource: image.ImageSource = image.createImageSource(rawFileDescriptor);
  const pixelMap: Promise<PixelMap> = imageSource.createPixelMap({
    editable: false,
    desiredPixelFormat: image.PixelMapFormat.BGRA_8888,
    desiredSize: { width: w, height: h }
  });
  imageSource.release()
  return pixelMap;
}
  1. 从pixelMap数据中取出颜色信息数组,将每个像素的透明度单独提取至allPixels,再转化生成为对应形状的二维数组掩码maskArray。
// entry/src/main/ets/utils/WindowUtils.ets
export async function getPicMask(width: number, height: number, picPath: string): Promise<number[][]> {
  const maskArray: number[][] = new Array(height).fill(null).map(() => new Array(width).fill(0));
  const pixelMap: image.PixelMap = await image2PixelMap(picPath, width, height);
  const pixelArrayBuffer: ArrayBuffer = new ArrayBuffer(width * height * 4);
  await pixelMap.readPixelsToBuffer(pixelArrayBuffer);
  const allPixels: number[] = [];
  const unit8Pixels: Uint8Array = new Uint8Array(pixelArrayBuffer);
  for (let i = 0, j = 0; i < unit8Pixels.length; i += 4, j++) {
    // unit8Pixels[i+3] is alpha channel of BGRA_8888
    allPixels[j] = unit8Pixels[i + 3] > 0 ? 1 : 0;
  }
  pixelMap.release()
  let k = 0;
  for (let i = 0; i < width; i++) {
    for (let j = 0; j < height; j++) {
      maskArray[i][j] = allPixels[k++];
    }
  }
  return maskArray;
}
  1. 使用setWindowMask(windowMask)接口,设置窗口形状。
// entry/src/main/ets/utils/WindowUtils.ets
async function setWindowShape(win: window.Window, width: number,
  height: number, getMaskFunc: (w: number, h: number, picPath?: string) => Promise<number[][]>,
  picPath?: string): Promise<void> {
  const windowMask = await getMaskFunc(width, height, picPath);
  if (canIUse('SystemCapability.Window.SessionManager')) {
    win.setWindowMask(windowMask);
  } else {
    hilog.info(0x0000, 'Simple', 'can not use SessionManager syscap');
  }
}

/**
 * Set the window with pic shape
 * @param win The target window
 * @param width The original window width
 * @param height The original window height
 * @param picPath The pic path in rawfile
 */
export async function setWindowPicShape(win: window.Window, width: number,
  height: number, picPath: string): Promise<void> {
  setWindowShape(win, width, height, (w, h, picPath) => {
    return getPicMask(w, h, picPath!)
  }, picPath)
}
  1. 使用showWindow()接口显示异形子窗口。
// entry/src/main/ets/pages/Index.ets
subWindow.showWindow();

实现效果

图4 不规则形状子窗口效果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值