接入 HarmonyOS NEXT

本文介绍如何将小程序组件接入到 HarmonyOS NEXT 客户端。您可以基于已有工程使用 ohpmrc 方式接入小程序 SDK 到客户端。

前置条件

添加小程序 SDK 前,请确保已经接入工程到 mPaaS。更多信息请参见 接入 mPaaS 能力

引入依赖

在项目的 .ohpmrc 中添加如下仓库。

@mpaas:registry=https://mpaas-ohpm.oss-cn-hangzhou.aliyuncs.com/meta

添加 SDK

通过 使用 mppm 工具 安装小程序组件。

image

配置权限

module.json5 中配置所需权限。

"requestPermissions":[
  {
  "name" : "ohos.permission.GET_NETWORK_INFO",
  },
  {
  "name" : "ohos.permission.INTERNET",
  },
  { // 根据需要,如果有使用到扫码功能,需要添加
        "name": "ohos.permission.CAMERA",
        "reason": "$string:internet_reason",
        "usedScene": {}
  },
  { // 根据需要,如果有使用到获取位置功能,需要添加
        "name": "ohos.permission.LOCATION",
        "reason": "$string:internet_reason",
        "usedScene": {
        }
      },
      {
        "name": "ohos.permission.LOCATION_IN_BACKGROUND",
        "reason": "$string:internet_reason",
        "usedScene": {
        }
  },
  { // 根据需要,剪贴板权限
        "name": "ohos.permission.READ_PASTEBOARD",
        "reason": "$string:internet_reason",
        "usedScene": {
        }
      },
  {// 根据需要,保存图片接口需要的权限
        "name": "ohos.permission.WRITE_IMAGEVIDEO",
        "reason": "$string:internet_reason",
        "usedScene": {
        }
      },
]

使用 SDK

初始化

在 mPaaS 框架初始化完成之后,初始化 HRiverMini

HRiverMini.init()
重要

使用 SDK 前必须初始化 mPaaS 框架,并设置 userId 用于后续的预览/真机调试。

mPaaS 框架初始化可参考 初始化 mPaaS,示例如下:

import AbilityStage from '@ohos.app.ability.AbilityStage';
import { MPFramework } from '@mpaas/framework';

export default class ModuleEntry extends AbilityStage {
  async onCreate() {
    MPFramework.create(this.context);
    
    MPFramework.instance.userId = 'MPTestCase'
  }
}

打开小程序

小程序的页面路由基于 Navigation,需要在 Navigation 页面的 aboutToAppear() 添加以下配置:

aboutToAppear() {
  HRiverMini.notifyNavigationCreate(this.context, this.pageInfos)
}

添加 Navigation:

@Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack()
@Builder
  PageMap(name: string, navPageIntent: Map<string, Object>) {
    AppPage(name, navPageIntent);
  }

build() {
  Navigation(this.pageInfos) {
      Column() {
      ...
      }
      .height('100%')
      .width('100%')
  }.navDestination(this.PageMap);
}

启动小程序 API:

let startParams = new Map<string, Object>() // 启动参数
HRiverMini.startApp("2023112713520001", startParams)

启动小程序并跳转到指定页面:

let startParams = new Map<string, Object>()
startParams.set("page", "/page/component/view/view");
HRiverMini.startApp("2020012000000001", startParams)

注册自定义 API

HRiverMini.init() 初始化之前调用以下 API:

HRiverMini.registerExtension(()=> {
      import('@mpaas/hriverminidemo/src/main/ets/component/CustomExtension')
      import('@mpaas/hriverminidemo/src/main/ets/component/CustomExtension1')
      import('@mpaas/hriverminidemo/src/main/ets/component/CustomExtension2')
    })

import 即动态引入自定义 API 的 Extension

CustomExtension 的实现如下:

import { ApiBaseExtension,
  BridgeCallback,
  defineJSAPIClass, ExtensionParameter,
  MyExtHubContext,
  registerJSAPI, required } from '@alipay/exthub'

@defineJSAPIClass(():ApiBaseExtension => {return new CustomExtension()})
export class CustomExtension extends ApiBaseExtension {
  /**
   * 自定义Api: customApi
   * @param param
   * @param context
   * @param callback
   */
  @registerJSAPI
  customApi(@required(ExtensionParameter.CallParameters) param: Record<string, Object>,
    @required(ExtensionParameter.MyExtHubContext) context: MyExtHubContext,
    @required(ExtensionParameter.BridgeCallback) callback: BridgeCallback) {
    // 参数从param读取
    // 上下文如页面context等从context中读取
    // 回调给小程序使用callback

    // 调用成功后回调具体数据
    callback.sendSuccessResponse({
      data: 'apiSuccess'
    })
  }

  /**
   * 自定义Api: customApi2
   * @param param
   * @param context
   * @param callback
   */
  @registerJSAPI
  customApi2(@required(ExtensionParameter.CallParameters) param: Record<string, Object>,
    @required(ExtensionParameter.MyExtHubContext) context: MyExtHubContext,
    @required(ExtensionParameter.BridgeCallback) callback: BridgeCallback) {
    // 失败的回调通知
    callback.sendErrorResponse(-1, "call failed msg")
  }
}

Native 通知小程序

import { EngineUtils, HRiverMiniEngine } from '@mpaas/hrivermini'

HRiverMiniEngine.sendToRender(EngineUtils.getPageFromContext(context)?.getRender() || null, 'event', {})

扫码预览/真机调试

方式一:直接调起扫码。

let startParams = new Map<string, Object>()
HRiverMini.scan(getContext(this), startParams)

方式二:通过其他扫码,打开扫码结果。

let startParams = new Map<string, Object>()
HRiverMini.scanByUri(scanResult, startParams) // scanResult为扫码结果的string

配置小程序包请求时间间隔

mPaaS 支持配置小程序包的请求时间间隔,可全局配置。

HRiverMini.setAsyncReqRate("{\"config\":{\"al\":\"3\",\"pr\":{\"4\":\"86400\",\"common\":\"864000\"},\"ur\":\"1800\",\"fpr\":{\"common\":\"3888000\"}},\"switch\":\"yes\"}")

其中 \"ur\":\"1800\" 是设置全局更新间隔的值,1800 为默认值,代表间隔时长,单位为秒,您可修改此值来设置您的全局小程序包请求间隔,范围为 0 ~ 86400 秒(即 0 ~ 24 小时,0 代表无请求间隔限制)。

启动参数

key

value

描述

query

示例:a=xx&c=xx

小程序传参

page

示例:pages/twoPage/twoPage

指定打开的页面并传参

nbupdate

synctry、async,默认 async

  • synctry:强制更新版本

  • async: 不强制更新版本

disablePresetMenu

YES/NO,默认 NO

是否隐藏胶囊

设置 userAgent

HRiverMini.setUserAgent(ua) // 会拼接在默认ua后面

更新小程序

HRiverMini.updateApp(appId)

预置小程序

  1. 在工程的 rawfile 下增加 nebulaPresetnebulapresetinfo 目录。其中 nebulaPreset 目录下放置内置的所有小程序包,文件名为对应的 appidnebulapresetinfo 目录下放置 customnebulapreset.json 文件,文件内容就是内置小程序的 JSON 配置内容,JSON 格式和其他平台稍有区别,只保留 data 下内容即可。

    示例如下:

    image

    customnebulapreset.json 内容如下:

    [
    		{
    			"app_desc":"小程序示例",
    			"app_id":"9999999988888888",
    			"auto_install":1,
    			"extend_info":{
    				"launchParams":{
    					"enableTabBar":"NO",
    					"enableKeepAlive":"NO",
    					"enableDSL":"YES",
    					"nboffline":"sync",
    					"enableWK":"YES",
    					"page":"pages/index/index",
    					"tinyPubRes":"YES",
    					"enableJSC":"YES"
    				},
    				"usePresetPopmenu":"YES"
    			},
    			"fallback_base_url":"https://xxx.com/xxxx/nebula/fallback/",
    			"global_pack_url":"",
    			"installType":1,
    			"main_url":"/index.html#pages/index/index",
    			"name":"yttest",
    			"online":1,
    			"package_url":"https://xxx.com/xxx/nebula/9999999988888888_1.0.0.0.amr",
    			"patch":"",
    			"sub_url":"",
    			"version":"1.0.0.0",
    			"vhost":"https://9999999988888888.h5app.com"
    		},
        {
    			"app_desc":"小程序示例2",
    			"app_id":"9999999988888889",
    			"auto_install":1,
    			"extend_info":{
    				"launchParams":{
    					"enableTabBar":"NO",
    					"enableKeepAlive":"NO",
    					"enableDSL":"YES",
    					"nboffline":"sync",
    					"enableWK":"YES",
    					"page":"pages/index/index",
    					"tinyPubRes":"YES",
    					"enableJSC":"YES"
    				},
    				"usePresetPopmenu":"YES"
    			},
    			"fallback_base_url":"https://xxx.com/xxxx/nebula/fallback/",
    			"global_pack_url":"",
    			"installType":1,
    			"main_url":"/index.html#pages/index/index",
    			"name":"yttest",
    			"online":1,
    			"package_url":"https://xxx.com/xxx/nebula/9999999988888889_1.0.0.0.amr",
    			"patch":"",
    			"sub_url":"",
    			"version":"1.0.0.0",
    			"vhost":"https://9999999988888889.h5app.com"
    		},
    	]
  2. 初始化之后调用 HRiverMini.loadPresetApp

    HRiverMini.loadPresetApp((appId: string) => {
        // 每安装成功一个都会回调一次并返回appId
        hilog.debug(1, "MiniTag", "installPreset: " + appId)
    })

小程序信息管理

  1. 根据 appId 获取小程序信息。返回的信息包括 appIdversiontitle

    let result: ESObject = HRiverMini.getAppInfo('1122334455667788')
    if (result) {
        // result.appId 小程序id
        // result.version 小程序版本
        // result.title 小程序名称
        hilog.debug(1, "MiniTag", `getAppInfo: ${result.appId} ${result.version}`)
    }
  2. 删除小程序信息。

    HRiverMini.deleteAppInfo('1122334455667788')

切面事件拦截

通过 HRiverMiniregisterPoint 注入拦截切面,针对部分事件做监听和拦截。

/**
* 注册拦截Point
* extensionName: 具体实现的Extension的name
* pointArr: 具体拦截事件的数组
**/
registerPoint(extensionName: string, pointArr: Array<string>)

返回事件拦截

事件名称:CRV_PROTOCOL_XRiverPageBackIntercept

具体实现如下:

{ CRV_PROTOCOL_XRiverPageBackIntercept } from '@mpaas/xriverohos';

// PageInterceptExtension为自定义的切面实现,参考demo。 CRV_PROTOCOL_XRiverPageBackIntercept为返回事件的key
HRiverMini.registerPoint(PageInterceptExtension.name, [CRV_PROTOCOL_XRiverPageBackIntercept])

其中 PageInterceptExtension 实现如下:

import {
  CRV_PROTOCOL_XRiverPageBackIntercept,
  defineExtensionConstructor, Extension, ExtensionContext,
  Page,
  PageBackInterceptPoint } from '@mpaas/xriverohos';

@defineExtensionConstructor((): Extension => {
  return new PageInterceptExtension();
})

// 继承Extension并且实现PageBackInterceptPoint
export class PageInterceptExtension extends Extension implements PageBackInterceptPoint{
  constructor() {
    super();
    //注册CRV_PROTOCOL_XRiverPageBackIntercept的拦截事件的对应方法名,返回事件固定为interceptBackEvent
    this.registerProtocolFunction(CRV_PROTOCOL_XRiverPageBackIntercept, 'interceptBackEvent');
  }

  // 实现interceptBackEvent方法,返回true表示拦截,false表示不拦截。
  // 以下方法体实现为demo示例,实际是否拦截需要根据业务情况
  interceptBackEvent(context: ExtensionContext): boolean {
    let page = context.getCurrentNode() as Page
    if (page.isFirstPage()) {
      return true
    }

    return false
  }

}

自定义UI

支持自定义标题栏、菜单栏、权限弹窗、小程序加载和错误页。需要在 oh-package.json5 中添加 antui 依赖:

"@mpaas/antui": "1.0.240717191810",
"@mpaas/nebulaintegration": "1.0.240718171401",
"@mpaas/xriverohos": "1.0.240718174425",

初始化通过设置 MYNavigationBarAdapter 实现类自定义标题栏、更多菜单弹窗、权限弹窗、小程序加载和错误页。

TinyAdapterUtils.setProvider(MYNavigationBarAdapter.name, new DemoNavBarAdapter())

DemoNavBarAdapter.ets 实现如下:

import { CRVPage, MYNavigationBarAdapter } from '@mpaas/nebulaintegration';
import { TinyMenuState } from '@mpaas/xriverohos';
import { DemoMenuCustomDialog } from './DemoMenuCustomDialog';
import { DemoNavComponent } from './DemoNavComponent';

export class DemoNavBarAdapter extends MYNavigationBarAdapter {
  // 自定义菜单弹窗UI
  getMenuCustomDialog(): WrappedBuilder<[TinyMenuState, CustomDialogController, CRVPage]> {
    return wrapBuilder(customDialogBuilder);
  }

  // 自定义标题栏UI
  getNavBarComponent(): WrappedBuilder<[ESObject]> | undefined {
    return wrapBuilder(customNavComponentBuilder);
  }

  /** 自定义加载和错误页
   * @param data: {appId: 小程序的appId, appInfo: loading的appInfo,参考EntryInfo, loadingProgress: 加载进度, errorCode: 错误码, rightButtonState: 按钮状态数据}
   * @returns
   */
  getLoadingComponent(): WrappedBuilder<[ESObject]> | undefined {
    return wrapBuilder(customLoadingComponentBuilder);
  }

  /**
   * 自定义权限弹窗
   * @param component 页面的component
   * @param dlgData {app: 小程序信息, scope: 权限的scope,例如scope.bluetooth ,icon: 小程序logo, title:小程序标题, desc: 弹窗内容,如:'使用你的蓝牙'}
   * @param reject 拒绝的回调方法
   * @param agree  同意的回调方法
   * @returns true: 自定义弹窗; false: 使用默认弹窗
   */
  showPermissionDialog(component: Object, dlgData: ESObject, reject: Function, agree: Function): boolean {
    AUPanelManager.showPermissionFrom(component, {
      icon: dlgData.icon,
      title: dlgData.title,
      subTitle: '申请',
      content: dlgData.desc,
      subContent: '',
      checkbox: undefined,
      buttons: [
        {
          title: '拒绝', type: DialogButtonType.Cancel, action: (isChecked: boolean) => {
          reject()
        }
        },
        {
          title: '允许', type: DialogButtonType.Normal, action: () => {
          agree()
        }
        }
      ],
      info: new ObservedPermissionInfo()
    })
    return true;
  }
}

@Builder
export function customDialogBuilder(state: TinyMenuState, controller: CustomDialogController, page: CRVPage) {
  // 菜单弹窗UI实现
  DemoMenuCustomDialog({
    tinyMenuState: state,  // 菜单数据
    customDialogController: controller, // 自定义弹窗controller
    page: page // 小程序当前page,可以获取小程序相关数据
  })
}

@Builder
export function customNavComponentBuilder(data: ESObject) {
  // 标题栏实现
  DemoNavComponent({
    needHideBackButton: data.needHideBackButton, // 是否显示返回键
    navigationBarState: data.navigationBarState, // 标题栏的具体数据
    page: data.page // 小程序当前page,可以获取小程序相关数据
  })
}


@Builder
export function customLoadingComponentBuilder(data: ESObject) {
  DemoLoadingComponent({
    appId: data.appId,
    appInfo: data.appInfo,
    loadingProgress: data.loadingProgress,
    errorCode: data.errorCode,
    rightButtonState: data.rightButtonState
  })
}

支持自定义标题栏

DemoNavComponent.ets 实现如下:

import { AUCustomIcon, AUIcon, IconFontKey } from '@mpaas/antui'
import { CRVPage } from '@mpaas/nebulaintegration'
import { CapsuleState, FrontColor, NavigationBarState, NavigationBarUtils } from '@mpaas/xriverohos'

@Component
export struct DemoNavComponent {

  page ?: CRVPage  // 当前page

  needHideBackButton: boolean = false; // 是否显示返回键

  @ObjectLink navigationBarState: NavigationBarState // 标题栏数据

  aboutToAppear(): void {
  }

  aboutToDisappear() {
  }

  build() {
      Row() {
        // Back button
        if(!this.needHideBackButton) {
          Button({
            type: ButtonType.Normal,
            stateEffect: true
          }) {
            AUIcon({
              icon: IconFontKey.ICONFONT_BACK,
              fontSize: 22,
              fontColor: this.navigationBarState.backButtonIconColor
            })
              .margin({
                top: 11,
                right: 2,
                bottom: 11,
                left: 0
              })
              .onAreaChange((oldValue: Area, newValue: Area): void => {
                this.navigationBarState.backButtonIconArea = newValue
              })
          }
          .visibility((this.navigationBarState.backButtonVisibility === 0) ? Visibility.Visible :
          Visibility.None)
          .backgroundColor('#00000000')
          .margin({
            top: 0,
            right: 0,
            bottom: 0,
            left: 8
          })
          .onClick((event: ClickEvent) => {
            // 返回键点击
            if (this.navigationBarState.backButtonOnClickListener !== undefined) {
              this.navigationBarState.backButtonOnClickListener(event)
            }
          })
          .onAreaChange((oldValue: Area, newValue: Area): void => {
            this.navigationBarState.backButtonInteractiveArea = newValue
          })
        }

        // Left close button
        Button({
          type: ButtonType.Normal,
          stateEffect: true
        }) {
          AUIcon({
            icon: IconFontKey.ICONFONT_AD_CLOSE,
            fontSize: 22,
            fontColor: this.navigationBarState.leftCloseButtonIconColor
          })
            .margin({ top: 11, right: 5, bottom: 11, left: 5 })
        }
        .visibility((this.navigationBarState.leftCloseButtonVisibility === 0) ? Visibility.Visible : Visibility.None)
        .backgroundColor('#00000000')
        .margin({ top: 0, right: 0, bottom: 0, left: 3 })
        .onClick((event) => {
          // 关闭按钮
          if (this.navigationBarState.leftCloseButtonOnClickListener !== undefined) {
            this.navigationBarState.leftCloseButtonOnClickListener(event)
          }
        })

        // Home button
        Button({
          type: ButtonType.Normal,
          stateEffect: true
        }) {
          AUCustomIcon({
            text: "\ue67d",
            fontSrc: $rawfile('tiny_iconfont.ttf'),
            fontSize: 22,
            fontColor: this.navigationBarState.homeButtonIconColor
          })
            .margin({ top: 11, right: 2, bottom: 11, left: 2 })
            .onAreaChange((oldValue: Area, newValue: Area): void => {
              this.navigationBarState.homeButtonIconArea = newValue
            })
        }
        .visibility((this.navigationBarState.homeButtonVisibility === 0) ? Visibility.Visible : Visibility.None)
        .backgroundColor('#00000000')
        .margin({ top: 0, right: 0, bottom: 0, left: 8 })
        .onClick((event) => {
          // 回到首页按钮点击
          if (this.navigationBarState.homeButtonOnClickListener !== undefined) {
            this.navigationBarState.homeButtonOnClickListener(event)
          }
        })
        .onAreaChange((oldValue: Area, newValue: Area): void => {
          this.navigationBarState.homeButtonInteractiveArea = newValue
        })

        // Title
        Row() {

          Column() {
            // Title text
            Text(this.navigationBarState.titleText)
              .visibility((this.navigationBarState.titleVisibility === 0) ? Visibility.Visible : Visibility.None)
              .fontSize(18)
              .fontStyle(FontStyle.Normal)
              .fontColor(this.navigationBarState.titleTextColor)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
                // .textOverflow({ overflow: TextOverflow.Clip })
              .maxLines(1)
              .onClick((clickEvent: ClickEvent): void => {
                // 标题点击
                if (this.navigationBarState.titleOnClickListener !== undefined) {
                  this.navigationBarState.titleOnClickListener(clickEvent)
                }
              })
          }
          .alignItems(HorizontalAlign.Start)

        }
        .width(0)
        .layoutWeight(1)
        .margin({
          top: 0,
          right: 0,
          bottom: 0,
          left: ((this.navigationBarState.backButtonVisibility === 1)
            && (this.navigationBarState.leftCloseButtonVisibility === 1)
            && (this.navigationBarState.homeButtonVisibility === 1)) ? 16 : 6
        })

        // // Placeholder
        // Blank()
        //   .layoutWeight(1)

        // Right buttons
        if (this.navigationBarState.capsuleState.visibility === 0) { // WTF: Conditional rendering here is not working...
          // Capsule style
          RightButtonComponent({
            capsuleState: this.navigationBarState.capsuleState
          })
            .animation({ duration: 300 })
        }
      }
      .visibility((this.navigationBarState.visibility === 0) ? Visibility.Visible : Visibility.None)
      .width('100%')
      .height('100%')
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.Start)
      .backgroundColor(NavigationBarUtils.alterColorWithAlpha(
        this.navigationBarState.backgroundColor,
        this.navigationBarState.backgroundAlpha))
      .hitTestBehavior(this.navigationBarState.penetrable ? HitTestMode.Transparent : HitTestMode.Default)
      .borderStyle(BorderStyle.Solid)
      .borderWidth({
        top: 0,
        right: 0,
        bottom: (this.navigationBarState.bottomLineVisibility === 0) ? '1px' : 0,
        left: 0
      })
      .borderColor(NavigationBarUtils.alterColorWithAlpha(
        this.navigationBarState.bottomLineColor,
        this.navigationBarState.bottomLineAlpha))
    }

}

@Component
export struct RightButtonComponent {

  aboutToAppear(): void {
  }

  @ObjectLink capsuleState: CapsuleState

  build() {
    // Capsule with more and close buttons
    Row() {
      // More button
      AUIcon({
        icon: this.capsuleState.moreButtonIconfont as IconFontKey,
        fontSize: 22,
        fontColor: (this.capsuleState.frontColor === FrontColor.Black) ? '#FF333333' : '#FFFFFFFF'
      })
        .visibility((this.capsuleState.moreButtonVisibility === 0) ? Visibility.Visible : Visibility.None)
        .margin({
          top: '4vp',
          right: '11vp',
          bottom: '4vp',
          left: '11vp'
        })
        .onClick((clickEvent: ClickEvent) => {
          // 更多菜单点击
          if (this.capsuleState.moreButtonOnClickListener !== undefined) {
            this.capsuleState.moreButtonOnClickListener(clickEvent)
          }
        })

      // Divider
      Row()
        .borderStyle(BorderStyle.Solid)
        .borderWidth('1px')
        .borderColor('#1A000000')
        .width('1px')
        .height('22vp')

      // Close button
      AUIcon({
        icon: this.capsuleState.closeButtonIconfont as IconFontKey,
        fontSize: 22,
        fontColor: (this.capsuleState.frontColor === FrontColor.Black) ? '#FF333333' : '#FFFFFFFF'
      })
        .visibility((this.capsuleState.closeButtonVisibility === 0) ? Visibility.Visible : Visibility.None)
        .margin({
          top: '4vp',
          right: '11vp',
          bottom: '4vp',
          left: '11vp'
        })
        .onClick((clickEvent: ClickEvent) => {
          // 关闭按钮点击
          if (this.capsuleState.closeButtonOnClickListener !== undefined) {
            this.capsuleState.closeButtonOnClickListener(clickEvent)
          }
        })
    }
    .visibility((this.capsuleState.visibility === 0) ? Visibility.Visible : Visibility.Hidden)
    .borderStyle(BorderStyle.Solid)
    .borderWidth('1px')
    .borderColor('#1A000000')
    .borderRadius('10000px')
    .backgroundColor(this.capsuleState.frontColor === FrontColor.White ? '#16000000' : '#00000000')
    .margin({
      top: '9vp',
      right: '4vp',
      bottom: '9vp',
      left: 0
    })
    .onAreaChange((oldValue: Area, newValue: Area): void => {
      this.capsuleState.capsuleArea = newValue
    })

  }

}

支持自定义菜单

DemoMenuCustomDialog.ets 实现如下:

import { TinyMenuButtonState, TinyMenuState } from '@mpaas/xriverohos'
import { window } from '@kit.ArkUI'
import { AUCustomIcon } from '@mpaas/antui'
import { CRVPage } from '@mpaas/nebulaintegration'

/**
 * The tiny menu dialog.
 */
@CustomDialog
export struct DemoMenuCustomDialog {

  @ObjectLink tinyMenuState: TinyMenuState

  customDialogController?: CustomDialogController

  @State isFullScreen: boolean = false

  @State paddingBottom: Length = 0

  page?: CRVPage

  aboutToAppear(): void {
    window.getLastWindow(getContext(this)).then((win: window.Window) => {
      this.isFullScreen = win.getWindowProperties().isLayoutFullScreen
      let area = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR)
      if (area.visible && area.bottomRect.height > 0) {
        this.paddingBottom = px2vp(area.bottomRect.height)
      }
    })
  }

  build() {
    Column() {
      Row() {
        Image(this.tinyMenuState.appIconImageUrl)
          .width((this.tinyMenuState.appIconImageUrl && this.tinyMenuState.appIconImageUrl.length > 0) ? '35vp' : '2vp')
          .height('35vp')
          .borderRadius('50vp')
          .borderWidth('0px')
          .margin({
            top: '18vp',
            right: '8vp',
            left: '16vp',
            bottom: '18vp',
          })

        Text(this.tinyMenuState.appName)
          .fontSize(16)
          .fontStyle(FontStyle.Normal)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
      }
      .width('100%')
      .alignItems(VerticalAlign.Center)

      // Divider
      Row() {
        Row()
          .width('100%')
          .height('1px')
          .backgroundColor('#cccccc')
      }
      .width('100%')
      .margin({
        top: 0,
        right: '16vp',
        bottom: 0,
        left: '16vp'
      })

      // Top tiny menu buttons
      Row() {
        ForEach(
          this.tinyMenuState.tinyMenuButtonStateArrayTop,
          (tinyMenuButtonState: TinyMenuButtonState, index: number) => {
            TinyMenuButtonComponent({
              tinyMenuButtonState: tinyMenuButtonState,
              menuButtonOnClickListener: new MenuButtonOnClickListener((mid: string): void => {
                // Close this dialog first
                this.customDialogController?.close()

                // Trigger click event logic
                if (this.tinyMenuState.menuButtonOnClickListener) {
                  this.tinyMenuState.menuButtonOnClickListener(mid)
                }
              })
            })
          })
      }
      .width('100%')
      .height('95vp')

      // Bottom tiny menu buttons
      Scroll(new Scroller()) {
        Row() {
          ForEach(
            this.tinyMenuState.tinyMenuButtonStateArrayBottom,
            (tinyMenuButtonState: TinyMenuButtonState, index: number) => {
              TinyMenuButtonComponent({
                tinyMenuButtonState: tinyMenuButtonState,
                menuButtonOnClickListener: new MenuButtonOnClickListener((mid: string): void => {
                  // Close this dialog first
                  this.customDialogController?.close()

                  // Trigger click event logic
                  if (this.tinyMenuState.menuButtonOnClickListener) {
                    this.tinyMenuState.menuButtonOnClickListener(mid)
                  }
                })
              })
            })
        }
        .height('95vp')
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .align(Alignment.Start)

      Text('取消')
        .fontSize(18)
        .fontStyle(FontStyle.Normal)
        .fontColor('ff333333')
        .backgroundColor('#FFFFFF')
        .width('100%')
        .height('57vp')
        .textAlign(TextAlign.Center)
        .onClick((event: ClickEvent) => {
          this.customDialogController?.close()
        })

      Row()
        .width('100%')
        .height(this.isFullScreen ? this.paddingBottom : 0)
        .backgroundColor('#FFFFFF')
    }
    .width('100%')
    .backgroundColor('#fff5f4f3')
    .borderRadius({
      topLeft: '12vp',
      topRight: '12vp',
      bottomLeft: 0,
      bottomRight: 0
    })
  }

}

/**
 * Menu button in the tiny menu.
 */
@Component
struct TinyMenuButtonComponent {

  @ObjectLink tinyMenuButtonState: TinyMenuButtonState

  @ObjectLink menuButtonOnClickListener: MenuButtonOnClickListener

  build() {
    Column() {
      // Icon
      Row() {
        // Image
        Image(this.tinyMenuButtonState.image)
          .width('26vp')
          .height('26vp')
          .visibility(this.tinyMenuButtonState.image ? Visibility.Visible : Visibility.None)
          .objectFit(ImageFit.Contain)

        // Iconfont
        AUCustomIcon({
          text: this.tinyMenuButtonState.iconfont,
          fontSrc: $rawfile('tiny_iconfont.ttf'),
          fontSize: 26,
          fontColor: this.tinyMenuButtonState.iconfontColor
        })
          .visibility(this.tinyMenuButtonState.image ? Visibility.None : Visibility.Visible)
          .align(Alignment.Center)
      }
      .width('45vp')
      .height('45vp')
      .backgroundColor('#ffffff')
      .borderRadius('10vp')
      .borderStyle(BorderStyle.Solid)
      .borderWidth(0)
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.Center)

      // Title
      Text(this.tinyMenuButtonState.title)
        .fontColor('#333333')
        .fontSize('10vp')
        .margin({
          top: '2vp'
        })
        .maxLines(2)
        .ellipsisMode(EllipsisMode.END)
    }
    .width('65vp')
    .justifyContent(FlexAlign.Center)
    .onClick((clickEvent: ClickEvent): void => {
      if (this.menuButtonOnClickListener.onClickListener) {
        this.menuButtonOnClickListener.onClickListener(this.tinyMenuButtonState.mid)
      }
    })
  }

}

@Observed
class MenuButtonOnClickListener {

  public onClickListener: ((mid: string) => void) | undefined

  public constructor(onClickListener: ((mid: string) => void) | undefined) {
    this.onClickListener = onClickListener
  }

}
支持自定义加载和错误页

DemoLoadingComponent.ets 实现参考如下:

import { EntryInfo, RightButtonState } from '@mpaas/xriverohos';

type AnimationStep = () => void;

@Component
export struct DemoLoadingComponent {
  @State rightButtonState: RightButtonState = new RightButtonState();
  appId?: string = '' // 小程序id
  @Prop appInfo?: EntryInfo; // 加载信息
  @Prop loadingProgress: number = 0; // 加载进度
  @Prop errorCode: number = 0; // 错误码,非0表示错误
  
  @State progressRotateOptions: RotateOptions = {
    angle: 0,
    centerX: '50%',
    centerY: '50%',
  }
  private rotateAnimationStep1?: AnimationStep;
  private rotateAnimationStep2?: AnimationStep;
  private rotateAnimationStep3?: AnimationStep;

  build() {
      Column() {
        Row() {
          RightButtonComponent({
            rightButtonState: this.rightButtonState
          })
            .margin({
              top: 0,
              right: 12,
              bottom: 0,
              left: 0
            })
        }.width('100%').justifyContent(FlexAlign.End)

        Row().height('20%')
        Stack() {
          Progress({ value: this.loadingProgress, total: 100, type: ProgressType.Ring })
            .width(55)
            .height(55)
            .color(this.errorCode <= 0 ? 0x1677ff : 0xdddddd)
            .backgroundColor(0xdddddd)
            .style({ strokeWidth: 1 })
            .rotate(this.progressRotateOptions)
          Image(this.appInfo?.iconUrl).alt($r('app.media.loading_page_icon')).width(40).height(40).borderRadius(20)
        }

        Row().height(18)
        Text(this.appInfo?.title).fontColor(0x333333).fontSize(18).width('100%').textAlign(TextAlign.Center)

        if (this.errorCode > 0) {
          Row().height(21)
          Text('网络不给力').fontColor(0x333333).fontSize(20).width('100%').textAlign(TextAlign.Center)

          Row().height(10)
          Text('请稍后再试').fontColor(0xaaaaaa).fontSize(14).width('100%').textAlign(TextAlign.Center)
        }
      }
      .width('100%')
  }

  aboutToAppear(): void {
    // rotate animation config
    this.rotateAnimationStep1 = this.generateAnimateStep(
      this.generateAnimateParam(() => {
        this.rotateAnimationStep2?.()
      }),
      120
    )

    this.rotateAnimationStep2 = this.generateAnimateStep(
      this.generateAnimateParam(() => {
        this.rotateAnimationStep3?.()
      }),
      240
    )

    this.rotateAnimationStep3 = this.generateAnimateStep(
      this.generateAnimateParam(() => {
        // reset rotateOptions
        this.progressRotateOptions = {
          angle: 0,
          centerX: '50%',
          centerY: '50%',
        }
        // repeat animation step
        if (this.errorCode <= 0) {
          this.rotateAnimationStep1?.()
        }
      }),
      360
    )

    setTimeout(() => {
      this.rotateAnimationStep1?.()
    }, 1000)
  }

  aboutToDisappear(): void {
  }

  private generateAnimateStep(value: AnimateParam, angle: number): AnimationStep {
    return () => {
      animateTo(value, () => {
        this.progressRotateOptions = {
          angle: angle,
          centerX: '50%',
          centerY: '50%',
        }
      })
    }
  }

  private generateAnimateParam(event: () => void): AnimateParam {
    return {
      duration: 300,
      tempo: 1,
      curve: Curve.Linear,
      iterations: 1,
      playMode: PlayMode.Normal,
      onFinish: () => {
        setTimeout(event,5);
      }
    }
  }
}

@Component
export struct RightButtonComponent {

  @ObjectLink rightButtonState: RightButtonState

  build() {
    Button({
      type: ButtonType.Normal,
      stateEffect:false
    }) {
    }
    .visibility(Visibility.Visible)
    .backgroundColor('#00000000')
    .onClick((event) => {
      if (this.rightButtonState.onClickListener !== undefined) {
        this.rightButtonState.onClickListener(event);
      }
    })
  }

}

当前版本组件的特殊说明

  • 地图组件目前基于华为地图,支持的 API 包括 getCurrentLocationmoveToLocation,需要在华为地图官网申请使用。

  • 获取剪贴板内容需要 App 申请 ohos.permission.READ_PASTEBOARD 权限,该权限为 ACL 权限。

当前版本不支持的组件和 API

  • 文件 API:获取文件信息、获取文件列表、移除文件、删除文件

  • canvas2

  • 直播

  • 联系人

  • 隐藏键盘

地图组件支持情况

地图组件支持的 API

  • latitude

  • longitude

  • scale

  • markers

  • polyline

  • circles

  • polygon

  • include-points

地图组件不支持的 API

  • map 高级定制渲染:marker 的 customCallout 仅支持 type=2。

  • style 仅支持 type=3 的样式渲染,其他 type 不支持。

  • onRegionChange 的 type 仅支持 end,不支持 start。