初始化
在 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  | 
  | 
disablePresetMenu  | YES/NO,默认 NO  | 是否隐藏胶囊  | 
设置 userAgent
HRiverMini.setUserAgent(ua) // 会拼接在默认ua后面更新小程序
HRiverMini.updateApp(appId)开关配置
通过开关配置实现功能配置。
HRiverMini.setConfig(`${key}`, `${value}`)其中 key、value 以及相应说明如下:
key  | value  | 说明  | 
xriver_show_share_menu  | YES/NO,默认为 NO  | 小程序菜单是否显示分享按钮。  | 
预置小程序
在工程的
rawfile下增加nebulaPreset和nebulapresetinfo目录。其中nebulaPreset目录下放置内置的所有小程序包,文件名为对应的appid。nebulapresetinfo目录下放置customnebulapreset.json文件,文件内容就是内置小程序的 JSON 配置内容,JSON 格式和其他平台稍有区别,只保留 data 下内容即可。示例如下:

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" }, ]初始化之后调用
HRiverMini.loadPresetApp。HRiverMini.loadPresetApp((appId: string) => { // 每安装成功一个都会回调一次并返回appId hilog.debug(1, "MiniTag", "installPreset: " + appId) })
小程序信息管理
根据
appId获取小程序信息。返回的信息包括appId、version和title。let result: ESObject = HRiverMini.getAppInfo('1122334455667788') if (result) { // result.appId 小程序id // result.version 小程序版本 // result.title 小程序名称 hilog.debug(1, "MiniTag", `getAppInfo: ${result.appId} ${result.version}`) }删除小程序信息。
HRiverMini.deleteAppInfo('1122334455667788')删除所有小程序信息。
HRiverMini.deleteAllAppInfo()
切面事件拦截
通过 HRiverMini 的 registerPoint 注入拦截切面,针对部分事件做监听和拦截。
/**
* 注册拦截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
  }
}支持隐藏标题栏
小程序隐藏标题栏,可以全局控制或者根据小程序 ID 进行控制。
初始化之后增加 point 拦截:
HRiverMini.registerAppPoint(PageInterceptExtension.name, [CRV_PROTOCOL_SESSION_sessionDidStart])PageInterceptExtension.ets 实现如下:
import {
    CRV_PROTOCOL_SESSION_sessionDidStart,
    defineExtensionConstructor, Extension
} from '@mpaas/xriverohos';
import { CRVApp } from '@mpaas/nebulaintegration';
@defineExtensionConstructor((): Extension => {
  return new PageInterceptExtension();
})
export class PageInterceptExtension extends Extension{
  constructor() {
    super();
    this.registerProtocolFunction(CRV_PROTOCOL_SESSION_sessionDidStart, 'sessionDidStart');
  }
  sessionDidStart(app: CRVApp) {
    let originParam = app.getStartParams()
    // 如果要根据appId配置,这里可以做条件判断
    if (originParam) {
      originParam['transparent'] = new Boolean(true)
      originParam['forceDefaultBackground'] = new Boolean(true)
    }
  }
}自定义 UI
支持自定义标题栏、菜单栏、权限弹窗、小程序加载和错误页。初始化通过设置 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);
      }
    })
  }
}import { AUDialogManager } from "@mpaas/antui";
import { PermissionAdapter, PermissionType } from "@mpaas/mpaas_permission_fortress";
import { common, Want } from "@kit.AbilityKit";
export class DemoPermissionAdapter extends PermissionAdapter {
  // type: 表示申请的权限类型,具体值参考文档后续部分,close:回调方法,弹窗取消后需要调用,goToSettings参数:true表示跳转设置页面并消失 false表示直接消失
  showGuideDialog(type: PermissionType, close: (goToSettings: boolean) => void): boolean {
    // 自定义弹窗逻辑,以下只是示例
    AUDialogManager.showNotice({
      onClose: () => {
        close(false) // 回调
      },
      title: '权限申请 ' + type, // 标题,根据业务情况
      message: '',  // 弹窗内容
      buttons: [{
        title: $r('app.string.go_to_set'),
        action: () => {
          close(true) // 回调
          let context = getContext(this) as common.UIAbilityContext;
          let want: Want = {
            bundleName: 'com.huawei.hmos.settings',
            abilityName: 'com.huawei.hmos.settings.MainAbility',
            uri: 'application_info_entry',
            parameters: { pushParams: '' }
          };
          context.startAbility(want).then((data) => {
            console.info('前往设置授权页面');
          })
        }
      }]
    })
    return true
  }
}PermissionType 类型:
export declare enum PermissionType {
    None = 0,
    Location = 1, // 定位
    Location_Background = 2,
    Location_Approximately = 3,
    Camera = 4, // 相机
    MicroPhone = 5, // 录音
    BlueTooth = 6, // 蓝牙
    Health_Read = 7, // 健康数据
    Notification = 8, // 通知
    Pasteboard = 9, // 剪切板
    Calendar = 10, // 日历
    Media = 11, // 多媒体
    Photo_Write = 12, // 相册写
    Photo = 13, // 相册读
    Contacts = 14, // 通讯录
    Carrier = 15, // 运营商
    Vibrate = 16 // 震动
}自定义权限引导弹窗
定位等系统权限被禁止后,下次调用相应功能会有引导弹窗提示,可以参考以下方式自定义引导弹窗。
启动完成初始化之后设置自定义 provider。
import { PermissionAdapter, PermissionAdapterUtils } from '@mpaas/mpaas_permission_fortress';
...
...
PermissionAdapterUtils.setProvider(PermissionAdapter.name, new DemoPermissionAdapter())DemoPermissionAdapter.ets 实现如下:
import { AUDialogManager } from "@mpaas/antui";
import { PermissionAdapter, PermissionType } from "@mpaas/mpaas_permission_fortress";
import { common, Want } from "@kit.AbilityKit";
export class DemoPermissionAdapter extends PermissionAdapter {
  // type: 表示申请的权限类型,具体值参考文档后续部分,close:回调方法,弹窗取消后需要调用,goToSettings参数:true表示跳转设置页面并消失 false表示直接消失
  showGuideDialog(type: PermissionType, close: (goToSettings: boolean) => void): boolean {
    // 自定义弹窗逻辑,以下只是示例
    AUDialogManager.showNotice({
      onClose: () => {
        close(false) // 回调
      },
      title: '权限申请 ' + type, // 标题,根据业务情况
      message: '',  // 弹窗内容
      buttons: [{
        title: $r('app.string.go_to_set'),
        action: () => {
          close(true) // 回调
          let context = getContext(this) as common.UIAbilityContext;
          let want: Want = {
            bundleName: 'com.huawei.hmos.settings',
            abilityName: 'com.huawei.hmos.settings.MainAbility',
            uri: 'application_info_entry',
            parameters: { pushParams: '' }
          };
          context.startAbility(want).then((data) => {
            console.info('前往设置授权页面');
          })
        }
      }]
    })
    return true
  }
}PermissionType 类型:
export declare enum PermissionType {
    None = 0,
    Location = 1, // 定位
    Location_Background = 2,
    Location_Approximately = 3,
    Camera = 4, // 相机
    MicroPhone = 5, // 录音
    BlueTooth = 6, // 蓝牙
    Health_Read = 7, // 健康数据
    Notification = 8, // 通知
    Pasteboard = 9, // 剪切板
    Calendar = 10, // 日历
    Media = 11, // 多媒体
    Photo_Write = 12, // 相册写
    Photo = 13, // 相册读
    Contacts = 14, // 通讯录
    Carrier = 15, // 运营商
    Vibrate = 16 // 震动
}支持延迟初始化
隐私协议需求下,需要支持延迟初始化。延迟初始化需要在初始化时注入首页 UIAbility 信息。操作步骤如下:
创建
HomeAbilityContext类。import { common } from '@kit.AbilityKit'; import { window } from '@kit.ArkUI'; export class HomeAbilityContext { private mAbilityContext?: common.UIAbilityContext; private mNavPathStack?: NavPathStack; private mWindowStage?: window.WindowStage; constructor(abilityContext?: common.UIAbilityContext) { this.mAbilityContext = abilityContext; } setWindowStage(windowStage?: window.WindowStage) { this.mWindowStage = windowStage; } getWindowStage() { return this.mWindowStage; } setNavPathStack(navPathStack?: NavPathStack) { this.mNavPathStack = navPathStack; if (this.mAbilityContext) { this.mAbilityContext.eventHub.emit(navPathStack ? "FRAMEWORK_EVENT_NAV_PATH_STACK_ATTACH" : "FRAMEWORK_EVENT_NAV_PATH_STACK_DETACH"); } } getNavPathStack(): NavPathStack | undefined { return this.mNavPathStack; } }在入口的
EntryAbilityStage的onCreate中注册abilityLifecycle,并缓存TopUIAbility。let abilityLifecycleCallback = { onAbilityCreate(ability: UIAbility) { console.log(ability + ' onAbilityCreate.'); setTopAbility(ability) }, onAbilityDestroy(ability: UIAbility) { if (getTopAbility() === ability) { setTopAbility(undefined) } }, onAbilityForeground(ability: UIAbility) { console.log(ability + ' onAbilityForeground.'); setTopAbility(ability) }, onAbilityBackground(ability: UIAbility) { console.log(ability + ' onAbilityBackground.'); }, } as AbilityLifecycleCallback; this.context.getApplicationContext().getApplicationContext().on("abilityLifecycle", abilityLifecycleCallback)初始化小程序后确保在
HRiverMini.notifyNavigationCreate之前注入首页 context。let applicationManager: ESObject = Framework.microApplicationContext["applicationManger"] if (applicationManager) { applicationManager["mTopAbility"] = getTopAbility() // 获取第二步缓存的TopUIAbility if (applicationManager["mAbilityInfoMap"]) { let map: Map<common.UIAbilityContext, ESObject> = applicationManager["mAbilityInfoMap"] as Map<common.UIAbilityContext, ESObject> let homeContext = new HomeAbilityContext(this.context) homeContext.setWindowStage(getWindowStage()) map.set(this.context, homeContext) } }
当前版本组件的特殊说明
地图组件目前基于华为地图,支持的 API 包括
getCurrentLocation和moveToLocation,需要在华为地图官网申请使用。获取剪贴板内容需要 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。