【鸿蒙开发】鸿蒙ArkUI自定义组件如何封装一个好用的Toast/Loading/ProgressHUD组件

1. HUD

在移动端 App 开发中,Toast 、 Loading 和 Progress 是十分常用的UI控件,如果不做特殊要求,一般可以直接使用系统 API 提供的方法,但如果想要定制化 UI,就需要自定义实现了。

在 HarmonyOS 中,Toast 可以直接用 promptAction 实现,但是 Loading 和 Progress 并没有直接提供完善的组件封装(指 API11 及以前,API12 后未知)。

import promptAction from '@ohos.promptAction'

promptAction.showToast({            
  message: 'Message Info',
  duration: 2000 
})

在 iOS 的 UIKit 框架中,Toast 和 Loading 都是没有直接的 API 提供的,开发中一般会使用三方库,这类三方库大都统称为 HUD (Head Up Display) ,例如 MBProgressHUD 和 SVProgressHUD ,这里的 HUD 就是沿用了 iOS 端的命名习惯。

2. ArkUI 自定义组件: xt_hud

先上成果:

xt_hud 是我基于 ArkUI 框架封装的三方组件,适配 API11,具体的 API 符合原生开发使用习惯。

下载安装:

ohpm install @jxt/xt_hud

●Demo:

○gitee.com/kukumalu/xt…
○github.com/kukumaluCN/…

●具体 API 使用介绍:

好用的Toast/Loading/Progress自定义组件XTHUD

3. 自定义 HUD 组件的技术探索

HUD 这类组件,不同于 Button/Text 等常规的 ArkUI 组件,直接在对应父组件的构造函数 build() 中挂载、布局、交互即可,更多的情况是直接用在逻辑代码中。

如果 ArkUI 的自定义组件可以直接实例化对象,那我们就可以脱离父组件的 build() 挂载 UI 的过程,直接使用。

系统框架中的 promptAction 就是这样的 API,通过阅读其 API 文档,可以知道其大致是基于 CustomDialog 封装实现的。

import promptAction from '@ohos.promptAction'

promptAction.showToast({            
  message: 'Message Info',
  duration: 2000 
})

3.1. promptAction 的源码实现

系统的 promptAction 是怎么实现的呢?最简单的方式就是查看源码。

HarmonyOS Next 是基于 OpenHarmony 的,系统本身并不开源,但 OpenHarmony 是开源项目,源码仓库在这里:gitee.com/openharmony

我对于 OpenHarmony 了解并不太多,大致查了下,其中 ArkUI 最核心的源码仓库为:gitee.com/openharmony…

ArkUI开发框架源代码在 /foundation/arkui/ace_engine 下,目录结构如下图所示:

/foundation/arkui/ace_engine
├── adapter                       # 平台适配目录
│   ├── common
│   └── ohos
├── frameworks                    # 框架代码
│   ├── base                      # 基础库
│   ├── bridge                    # 前后端组件对接层
│   └── core                      # 核心组件目录

检索 PromptAction 类在如下目录中:

// arkui_ace_engine-master/frameworks/bridge/declarative_frontend/engine/jsUIContext.js

class PromptAction {
  /**
   * Construct new instance of PromptAction.
   * initialize with instanceId.
   * @param instanceId obtained on the c++ side.
   * @since 10
   */
  constructor(instanceId) {
    this.instanceId_ = instanceId;
    this.ohos_prompt = globalThis.requireNapi('promptAction');
  }

  showToast(options) {
    __JSScopeUtil__.syncInstanceId(this.instanceId_);
    this.ohos_prompt.showToast(options);
    __JSScopeUtil__.restoreInstanceId();
  }
  ...
}

可以看到,其本质是调用了的桥接层的 C++ 代码实现,参考价值并不太高:

// arkui_ace_engine-master/interfaces/napi/kits/promptaction/js_prompt_action.cpp

static napi_value PromptActionExport(napi_env env, napi_value exports)
{
    napi_value showMode = nullptr;
    napi_create_object(env, &showMode);
    napi_value prop = nullptr;
    napi_create_uint32(env, DEFAULT, &prop);
    napi_set_named_property(env, showMode, "DEFAULT", prop);
    napi_create_uint32(env, TOP_MOST, &prop);
    napi_set_named_property(env, showMode, "TOP_MOST", prop);

    napi_property_descriptor promptDesc[] = {
    DECLARE_NAPI_FUNCTION("showToast", JSPromptShowToast),
    DECLARE_NAPI_FUNCTION("showDialog", JSPromptShowDialog),
    DECLARE_NAPI_FUNCTION("showActionMenu", JSPromptShowActionMenu),
    DECLARE_NAPI_FUNCTION("openCustomDialog", JSPromptOpenCustomDialog),
    DECLARE_NAPI_FUNCTION("updateCustomDialog", JSPromptUpdateCustomDialog),
    DECLARE_NAPI_FUNCTION("closeCustomDialog", JSPromptCloseCustomDialog),
    DECLARE_NAPI_PROPERTY("ToastShowMode", showMode),
};
    NAPI_CALL(env, napi_define_properties(env, exports, sizeof(promptDesc) / sizeof(promptDesc[0]), promptDesc));
    return exports;
}

static napi_module promptActionModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = PromptActionExport,
.nm_modname = "promptAction",
.nm_priv = ((void*)0),
.reserved = { 0 },
};

// arkui_ace_engine-master/interfaces/napi/kits/promptaction/prompt_action.cpp

napi_value JSPromptShowToast(napi_env env, napi_callback_info info)
{
    TAG_LOGD(AceLogTag::ACE_DIALOG, "show toast enter");
    ...
}

3.2. ArkUI 组件的本质

通过查看 API 文档,可知 ArkUI 组件本质是 TS 的 class ,继承于 CommonMethod 基类。

declare class ColumnAttribute extends CommonMethod<ColumnAttribute>
declare class TextAttribute extends CommonMethod<TextAttribute>
declare class ButtonAttribute extends CommonMethod<ButtonAttribute>

declare class CommonMethod<T>

那么我们自定义的组件,理论上也可以直接通过 new 的方式去构造一个组件实例,并直接通过其实例对象执行方法。

3.3. new 一个自定义组件

假设我们自定义了一个 HUD 组件,并为其添加了 showToast 方法:

@Component
struct HUD {
  showToast() {
    console.log('showToast')
  }
  build() {
    Text('toast')
  }
}

在其他地方使用:

Button('show toast')
  .onClick(() => {
    const hud = new HUD()
    hud.showToast()
  })

执行结果也确实符合我们的预期:

app Log: showToast

3.4. 自定义组件带属性构造时的报错问题

对于一个组件来说,仅有实例方法是不够的,逻辑层还需要一些属性,尤其是支持响应式更新的属性,以满足我们不同场景下的 UI 定制化逻辑。

@Component
struct HUD {
  @Prop text: string = ''

  showToast() {
    console.log('showToast: ', this.text)
  }

  build() {
    Text('toast')
  }
}


Button('show toast')
  .onClick(() => {
    const hud = new HUD({
      text: 'test'
    })
    hud.showToast()
  })

上述代码执行后会直接报错:

E     [ArkRuntime Log] TypeError: is not callable
E     [ArkRuntime Log]     at ViewPU (/Volumes/lxc/OpenHarmony-4.1-Release_harmony/harmony2/foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/engine/stateMgmt.js:4992:4992)
E     [Engine Log]Lifetime: 0.000000s
E     [Engine Log]Js-Engine: ark
E     [Engine Log]page: pages/HUD.js
E     [Engine Log]Error message: is not callable
E     [Engine Log]Stacktrace:
E     [Engine Log]    at ViewPU (/Volumes/lxc/OpenHarmony-4.1-Release_harmony/harmony2/foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/engine/stateMgmt.js:4992:4992)

通过报错信息可以知道自定义组件所对应的 class 实际上应该是继承或者可能是间接继承于 ViewPU 类的。

3.5. 通过方舟字节码探索自定义组件的实现原理

3.5.1. 解包

构建 Demo 项目的 entry 模块:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值