Skip to content

鸿蒙 NEXT 开发:一些工具的包装

ObservedArray 可观察的数组类

ts
@Observed
class ObservedArray<T> extends Array<T> {
  constructor(args: T[]) {
    if (args instanceof Array) {
      super(...args);
    } else {
      super(args);
    }
  }
}

纯对象转 Class

class-transformer

ets
@State obj2: BasicJsonStruct = plainToClassFromExist(new BasicJsonStruct(), {
  obj1: {
    obj2: {
      text: '这是obj2的初始值'
    }
  }
})

fill_class

深拷贝(ETS)

ts
function _deepCopy(obj: Object): Object {
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }
    if (Array.isArray(obj)) {
        let _copy: Object[] = [];
        for (let i = 0; i < obj.length; i++) {
            _copy[i] = _deepCopy(obj[i]);
        }
        return _copy;
    } else {
        let _copy: Record<string, Object> = {};
        let keys = Object.keys(obj);
        for (let i = 0; i < keys.length; i++) {
            let key: string = keys[i];
            if (Reflect.get(obj, key)) {
                _copy[key] = _deepCopy(Reflect.get(obj, key));
            }
        }
        return _copy;
    }
}

export function deepCopy<T>(obj: T): T {
    return _deepCopy(obj) as T;
}

单例全局上下文

ts
export class GlobalContext {
  private static instance: GlobalContext;
  private _objects = new Map<string, Object>();

  private constructor() {}

  public static getContext(): GlobalContext {
    if (!GlobalContext.instance) {
      GlobalContext.instance = new GlobalContext();
    }
    return GlobalContext.instance;
  }

  getObject(value: string): Object | undefined {
    return this._objects.get(value);
  }

  setObject(key: string, objectClass: Object): void {
    this._objects.set(key, objectClass);
  }
}

尺寸适配类

ts
import display from "@ohos.display";

const context = getContext(this);

/**
 * Design drawing width.
 */
const DESIGN_WIDTH = 360;

/**
 * Design drawing height.
 */
const DESIGN_HEIGHT = 780;

/**
 * Fits tools with different sizes and lengths.
 */
export class DimensionUtil {
  static adaptDimension(value: number): number {
    const deviceDisplay = display.getDefaultDisplaySync();
    const widthScale = deviceDisplay.width / DESIGN_WIDTH;
    const virtualHeight = widthScale * DESIGN_HEIGHT;
    const designDim = Math.sqrt(DESIGN_WIDTH * DESIGN_WIDTH + DESIGN_HEIGHT * DESIGN_HEIGHT);
    const virtualDim = Math.sqrt(deviceDisplay.width * deviceDisplay.width + virtualHeight * virtualHeight);
    return (virtualDim * value) / designDim; // 放缩后长度
  }

  static adaptDimensionVp(value: number): number {
    return px2vp(DimensionUtil.adaptDimension(value));
  }

  static getPx(value: Resource): number {
    const beforeVp = context.resourceManager.getNumber(value.id);
    return DimensionUtil.adaptDimension(beforeVp);
  }

  static getVp(value: Resource): number {
    const beforeVp = context.resourceManager.getNumber(value.id);
    return px2vp(DimensionUtil.adaptDimension(beforeVp));
  }

  static getFp(value: Resource): number {
    const beforeFp = context.resourceManager.getNumber(value.id);
    return px2fp(DimensionUtil.adaptDimension(beforeFp));
  }
}

用于懒加载的数据源封装

ts
export class BasicDataSource<T> implements IDataSource {
  private listeners: DataChangeListener[] = [];
  protected dataArray: T[] = [];

  // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }

  // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      this.listeners.splice(pos, 1);
    }
  }

  // 通知LazyForEach组件需要重载所有子组件
  notifyDataReload(): void {
    this.listeners.forEach((listener) => {
      listener.onDataReloaded();
    });
  }

  // 通知LazyForEach组件需要在index对应索引处添加子组件
  notifyDataAdd(index: number): void {
    this.listeners.forEach((listener) => {
      listener.onDataAdd(index);
    });
  }

  // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
  notifyDataChange(index: number): void {
    this.listeners.forEach((listener) => {
      listener.onDataChange(index);
    });
  }

  // 通知LazyForEach组件需要在index对应索引处删除该子组件
  notifyDataDelete(index: number): void {
    this.listeners.forEach((listener) => {
      listener.onDataDelete(index);
    });
  }

  totalCount() {
    return this.dataArray.length;
  }

  // 重新加载数据
  reload() {
    this.notifyDataReload();
  }

  getData(index: number) {
    return this.dataArray[index];
  }

  insertData(index: number, data: T) {
    this.dataArray.splice(index, 0, data);
    this.notifyDataAdd(index);
  }

  pushData(data: T) {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }

  removeData(index: number) {
    this.dataArray.splice(index, 1);
    this.notifyDataDelete(index);
  }
}

权限工具

ts
import { BusinessError } from "@kit.BasicServicesKit";
import { abilityAccessCtrl, bundleManager, PermissionRequestResult, Permissions } from "@kit.AbilityKit";

/**
 * 查询是否有单个权限
 * @param permission 单个权限字符串
 * @returns
 */
export async function checkAccessToken(permission: Permissions): Promise<boolean> {
  let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  let grantStatus: abilityAccessCtrl.GrantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;

  // 获取应用程序的accessTokenID
  let tokenId: number = 0;
  try {
    let bundleInfo: bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(
      bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
    );
    let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
    tokenId = appInfo.accessTokenId;
  } catch (error) {
    let err: BusinessError = error as BusinessError;
    console.error(`Failed to get bundle info for self. Code is ${err.code}, message is ${err.message}`);
  }

  // 校验应用是否被授予权限
  try {
    grantStatus = await atManager.checkAccessToken(tokenId, permission);
  } catch (error) {
    let err: BusinessError = error as BusinessError;
    console.error(`Failed to check access token. Code is ${err.code}, message is ${err.message}`);
  }

  return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
}

/**
 * 用于UIAbility拉起弹框请求用户授权
 * 如果用户拒绝授权,将无法再次拉起弹窗,需要用户在系统应用“设置”的界面中手动授予权限。
 * @param permissions 权限数组
 * @returns
 */
export async function requestPermissions(permissions: Array<Permissions>): Promise<boolean> {
  const context = getContext();
  let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  let ret = true;
  try {
    let data: PermissionRequestResult = await atManager.requestPermissionsFromUser(context, permissions);
    let grantStatus: Array<number> = data.authResults;
    let permissions1 = data.permissions;
    let length: number = grantStatus.length;
    for (let i = 0; i < length; i++) {
      if (grantStatus[i] === 0) {
        console.log(`${permissions1} permissions is success`);
      } else {
        console.log(`${permissions1} permissions is failed`);
        ret = false;
      }
    }
  } catch (e) {
    console.log(e.message);
    ret = false;
  }
  return ret;
}

图片选择工具

ts
import { picker } from "@kit.CoreFileKit";
import { BusinessError } from "@kit.BasicServicesKit";

export async function selectPicture(): Promise<string | undefined> {
  try {
    let photoSelectOptions = new picker.PhotoSelectOptions();
    photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
    photoSelectOptions.maxSelectNumber = 1;
    let photoPicker = new picker.PhotoViewPicker();
    return photoPicker
      .select(photoSelectOptions)
      .then((photoSelectResult: picker.PhotoSelectResult) => {
        if (photoSelectResult && photoSelectResult.photoUris && photoSelectResult.photoUris.length > 0) {
          let filePath = photoSelectResult.photoUris[0];
          // Log.info(this.TAG, 'PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + filePath);
          return filePath;
        }
        return "";
      })
      .catch((error: BusinessError) => {
        // Log.error(this.TAG, 'PhotoViewPicker.select failed with err: ' + err);
        return "";
      });
  } catch (err) {
    // Log.error(this.TAG, 'PhotoViewPicker failed with err: ' + err);
    return Promise.reject(err);
  }
  return "";
}

文件操作

通过路径写文件

ts
export function writeFileSync(filePath: string, buffer: ArrayBuffer | string): number {
  const file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  const writeLen = fs.writeSync(file.fd, buffer);
  fs.closeSync(file);
  return writeLen;
}

export async function writeFile(filePath: string, buffer: ArrayBuffer | string): Promise<number> {
  const file = await fs.open(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  const writeLen = await fs.write(file.fd, buffer);
  await fs.close(file);
  return writeLen;
}

文件上传工具

推荐 @ohos/commons-fileupload

窗口管理工具类

ts
import window from "@ohos.window";
import display from "@ohos.display";
import mediaquery from "@ohos.mediaquery";
import UIAbility from "@ohos.app.ability.UIAbility";

export default class GlobalContext extends AppStorage {
  static mainWin: window.Window | undefined = undefined;
  static mainWindowSize: window.Size | undefined = undefined;
}
/**
 * 窗口、屏幕相关信息管理类
 */
export class WindowManager {
  private static instance: WindowManager | null = null;
  private displayInfo: display.Display | null = null;
  private orientationListener = mediaquery.matchMediaSync("(orientation: landscape)");

  constructor() {
    this.orientationListener.on("change", (mediaQueryResult: mediaquery.MediaQueryResult) => {
      this.onPortrait(mediaQueryResult);
    });
    this.loadDisplayInfo();
  }

  /**
   * 设置主window窗口
   * @param win 当前app窗口
   */
  setMainWin(win: window.Window) {
    if (win == null) {
      return;
    }
    GlobalContext.mainWin = win;
    win.on("windowSizeChange", (data: window.Size) => {
      if (GlobalContext.mainWindowSize == undefined || GlobalContext.mainWindowSize == null) {
        GlobalContext.mainWindowSize = data;
      } else {
        if (GlobalContext.mainWindowSize.width == data.width && GlobalContext.mainWindowSize.height == data.height) {
          return;
        }
        GlobalContext.mainWindowSize = data;
      }

      let winWidth = this.getMainWindowWidth();
      AppStorage.setOrCreate<number>("mainWinWidth", winWidth);
      let winHeight = this.getMainWindowHeight();
      AppStorage.setOrCreate<number>("mainWinHeight", winHeight);
      let context: UIAbility = new UIAbility();
      context.context.eventHub.emit("windowSizeChange", winWidth, winHeight);
    });
  }

  static getInstance(): WindowManager {
    if (WindowManager.instance == null) {
      WindowManager.instance = new WindowManager();
    }
    return WindowManager.instance;
  }

  private onPortrait(mediaQueryResult: mediaquery.MediaQueryResult) {
    if (mediaQueryResult.matches == AppStorage.get<boolean>("isLandscape")) {
      return;
    }
    AppStorage.setOrCreate<boolean>("isLandscape", mediaQueryResult.matches);
    this.loadDisplayInfo();
  }

  /**
   * 切换屏幕方向
   * @param ori 常量枚举值:window.Orientation
   */
  changeOrientation(ori: window.Orientation) {
    if (GlobalContext.mainWin != null) {
      GlobalContext.mainWin.setPreferredOrientation(ori);
    }
  }

  private loadDisplayInfo() {
    this.displayInfo = display.getDefaultDisplaySync();
    AppStorage.setOrCreate<number>("displayWidth", this.getDisplayWidth());
    AppStorage.setOrCreate<number>("displayHeight", this.getDisplayHeight());
  }

  /**
   * 获取main窗口宽度,单位vp
   */
  getMainWindowWidth(): number {
    return GlobalContext.mainWindowSize != null ? px2vp(GlobalContext.mainWindowSize.width) : 0;
  }

  /**
   * 获取main窗口高度,单位vp
   */
  getMainWindowHeight(): number {
    return GlobalContext.mainWindowSize != null ? px2vp(GlobalContext.mainWindowSize.height) : 0;
  }

  /**
   * 获取屏幕宽度,单位vp
   * 预览器无法获取,可手动给360vp
   */
  getDisplayWidth(): number {
    return this.displayInfo != null ? px2vp(this.displayInfo.width) : 0;
  }

  /**
   * 获取屏幕高度,单位vp
   * 预览器无法获取,可手动给360vp
   */
  getDisplayHeight(): number {
    return this.displayInfo != null ? px2vp(this.displayInfo.height) : 0;
  }

  /**
   * 释放资源
   */
  release() {
    if (this.orientationListener) {
      this.orientationListener.off("change", (mediaQueryResult: mediaquery.MediaQueryResult) => {
        this.onPortrait(mediaQueryResult);
      });
    }
    if (GlobalContext.mainWin != null) {
      GlobalContext.mainWin.off("windowSizeChange");
    }
    WindowManager.instance = null;
  }
}

判断手机旋转方向

ts
import sensor from "@ohos.sensor";

// ...

sensor.on(sensor.SensorId.ACCELEROMETER, (data: sensor.AccelerometerResponse) => {
    let angle = 0;
    let magnitude = data.x * data.x + data.y * data.y;
    // Don't trust the angle if the magnitude is small compared to the y value
    if (magnitude * 4 >= data.z * data.z) {
        let OneEightyOverPi = 57.29577957855;
        angle = 90 - Math.round(Math.atan2(-data.y, data.x) * OneEightyOverPi);
        // normalize to 0 - 359 range
        while (angle >= 360) {
            angle -= 360;
        }
        while (angle < 0) {
            angle += 360;
        }
    }

    const orientation = this.convertAngle(angle)
    this.orientation(orientation);
    // ...
}, { interval: 500000000 });

// ...

function convertAngle(angle: number) {
    angle = angle % 360;
    Log.i(TAG, `angle ${angle}`);
    if (angle >= 120 && angle <= 240) {
        return window.Orientation.PORTRAIT;
    } else if (angle >= 30 && angle <= 150) {
        return window.Orientation.LANDSCAPE_INVERTED;
    } else if (angle >= 300 || (angle <= 60 && angle > 0)) {
        return window.Orientation.PORTRAIT_INVERTED;
    } else if (angle >= 210 && angle <= 330) {
        return window.Orientation.LANDSCAPE;
    } else {
        return window.Orientation.UNSPECIFIED;
    }
}

function orientation(orientation: number) {
    if (orientation == window.Orientation.PORTRAIT) {
        return "DeviceOrientation.portraitUp";
    } else if (orientation == window.Orientation.PORTRAIT_INVERTED) {
        return "DeviceOrientation.portraitDown";
    } else if (orientation == window.Orientation.LANDSCAPE) {
        return "DeviceOrientation.landscapeRight";
    } else if (orientation == window.Orientation.LANDSCAPE_INVERTED) {
        return "DeviceOrientation.landscapeLeft";
    } else {
        return null;
    }
}

强制旋转屏幕

ts
import window from "@ohos.window";

window.getLastWindow(this.ability?.context).then((windowClass) => {
  windowClass.setPreferredOrientation(window.Orientation.PORTRAIT);
});

App 与 H5 通信-JsBridge 封装

ets
/*
 * Index.ets
 * 网页与App交互的示例
 * 封装JSBridge
 */

import WebView from '@ohos.web.webview';
import JSBridge from './JsBridge';

@Entry
@Component
struct WebJSBridgeExample {
  webController: WebView.WebviewController = new WebView.WebviewController();
  private jsBridge: JSBridge = new JSBridge(this.webController);

  build() {
    Column() {
      Web({
        src: $rawfile('MainPage.html'),
        controller: this.webController
      })
        .javaScriptAccess(true)
        .javaScriptProxy(this.jsBridge.javaScriptProxy)
        .onPageBegin(() => {
          // 初始化JsBridge,向网页注入脚本
          this.jsBridge.initJsBridge();
        })

    }
  }
}
ets
// JsBridge.ets
import WebView from '@ohos.web.webview';
import promptAction from '@ohos.promptAction';
import hilog from '@ohos.hilog';

export interface CallType {
  call: (func: string, params: string) => void;
}

export interface JavaScriptItem {
  object: CallType;
  name: string;
  methodList: Array<string>;
  controller: WebviewController;
}

export interface ParamsItem {
  callID: number;
  data: ParamsDataItem;
}

export interface ParamsDataItem {
  name?: string;
  tel?: string;
}

/**
 * 定义需要注入到网页上的脚本
 */
export const code = `
  const JSBridgeMap = {};
  let callID = 0;

  function JSBridgeCallback (id, params){
    JSBridgeMap[id](params);
    JSBridgeMap[id] = null;
    delete JSBridgeMap[id];
  }

  window.ohosCallNative = {
    callNative(method, params, callback){
      const id = callID++;
      const paramsObj = {
          callID: id,
          data: params || null
      }
      JSBridgeMap[id] = callback || (() => {}); // 注册回调
      JSBridgeHandle.call(method, JSON.stringify(paramsObj));
    }
  }
`;

/**
 * Define bridge class connect WebView and ArkTS.
 */
export default class JsBridge {
  controller: WebView.WebviewController;

  constructor(controller: WebView.WebviewController) {
    this.controller = controller;
  }

  /**
   * Injects the JavaScript object into window and invoke the function in window.
   *
   * @returns javaScriptProxy object.
   */
  get javaScriptProxy(): JavaScriptItem {
    let result: JavaScriptItem = {
      object: { // 参与注册的对象。只能声明方法,不能声明属性。
        call: this.call
      },
      name: "JSBridgeHandle", // 注册对象的名称,与window中调用的对象名一致。
      methodList: ['call'], // 参与注册的应用侧JavaScript对象的方法。
      controller: this.controller // 组件的控制器
    }
    return result;
  }

  initJsBridge(): void {
    this.controller.runJavaScript(code);
  }

  /**
   * 例子:执行网页上的方法
   */
  runWebFunction() {
    const code = `webFunction()`; // 要在网页上执行的代码
    this.controller.runJavaScript(code, (error, result) => {
      if (error) {
        console.info(`run JavaScript error: ` + JSON.stringify(error))
      }
      if (result) {
        console.info(`The webFunction() return value is: ${result}`)
        promptAction.showToast({
          message: `The webFunction() return value is: ${result}`
        })
      }
    });
  }

  /**
   * 接收网页端的调用,通过方法名(func)分发
   */
  call = (func: string, params: string): void => {
    hilog.debug(0x0000, "call_func", func)
    hilog.debug(0x0000, "call_params", params)
    const paramsObject: ParamsItem = JSON.parse(params);
    let result: Promise<string> = new Promise((resolve) => resolve(''));
    switch (func) {
      default:
        break;
    }
    result.then((data: string) => {
      // callID 用于映射回调函数
      this.callback(paramsObject?.callID, data);
    })
  }

  /**
   * The ArkTS invoke the WebView by using runJavaScript.
   */
  callback = (id: number, data: string): void => {
    this.controller.runJavaScript(`JSBridgeCallback("${id}", ${JSON.stringify(data)})`);
  }
}

静态文件

html
<!-- rawfile/MainPage.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>testApp</title>
</head>
<body>
<div class="container">
    <h1>Web Page</h1>
    <div id="text"></div>
    <div id="text2"></div>
</div>
<script src="./js/mainPage.js"></script>
</body>
</html>
js
// rawfile/js/mainPage.js
function runAppFunctionExample() {
    // 调用App注入的方法
    window.ohosCallNative.callNative('yourFunctionName', {}, (data) => {
        // 执行App的回调
        document.getElementById('text2').innerText = JSON.stringify(data);
    })
}

function webFunction() {
    document.getElementById('text').innerText = '网页上的方法被调用了'
    return '返回值'
}

document.getElementById('text').innerText = 'test'
document.getElementById('text2').innerText = '3秒后自动调用App的方法'

setTimeout(() => {
    runAppFunctionExample()
}, 3000)

全局弹窗封装

CustomDialog 方案

创建自定义弹窗组件

ets
@CustomDialog
export struct CustomDialogView {
  @Link visible: boolean;
  controller: CustomDialogController;
  // 弹窗交互事件参数,点击确认和取消按钮时的回调函数
  onCancel?: () => void;
  onConfirm?: () => void;

  build() {
    Row() {
      Button()
        .onClick(() => {
          this.visible = false;
          this.onCancel?.();
        })
    }
  }
}

自定义组件 Dialog,对自定义弹窗组件进行二次封装。

ets
@Component
export struct Dialog {
  // 监听外部传入的visible变量,visible值发生变化时触发onChange回调函数
  @Watch("onChange") @Link visible: boolean;
  onCancel?: () => void;
  onConfirm?: () => void;
  // 通过CustomDialogController的builder参数绑定弹窗组件CustomDialogView
  private controller = new CustomDialogController({
    builder: CustomDialogView({
      visible: $visible,
      onCancel: this.onCancel,
      onConfirm: this.onConfirm,
    }),
  })

  /**
   * 当visible的值变化时触发回调
   */
  onChange(): void{
    if (this.visible) {
      this.controller.open();
    } else {
      this.controller.close();
    }
  }

  // 二次封装的Dialog组件主要通过控制器控制弹窗,不需要任何界面
  build() {
  }
}

使用方导入自定义组件 Dialog 并传入相应入参。

ets
@Component
export struct CustomDialog {
  // 外部定义visible变量作为弹窗组件入参,控制弹窗显隐
  @State visible: boolean = false;

  build() {
    Column({ space: 20 }) {
      Button()
        // 点击修改visible变量后,visible的值可以被Dialog组件监听并响应
        .onClick(() => this.visible = !this.visible)

      // 通过双向绑定visible变量,实现外部控制弹窗
      Dialog({
        visible: $visible,
      })
    }
  }
}

其它方案(子窗口)

https://gitee.com/wolfx/ohos_libs/tree/dev/package_utils/src/main/ets/common

最后编辑时间:

Version 4.2 (core-1.3.4)