本文介绍如何将小程序组件接入到 HarmonyOS NEXT 客户端。您可以基于已有工程使用 ohpmrc 方式接入小程序 SDK 到客户端。
前置条件
添加小程序 SDK 前,请确保已经接入工程到 mPaaS。更多信息请参见 接入 mPaaS 能力。
引入依赖
在项目的 .ohpmrc
中添加如下仓库。
@mpaas:registry=https://mpaas-ohpm.oss-cn-hangzhou.aliyuncs.com/meta
添加 SDK
通过 使用 mppm 工具 安装小程序组件。
配置权限
在 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 |
|
disablePresetMenu | YES/NO,默认 NO | 是否隐藏胶囊 |
设置 userAgent
HRiverMini.setUserAgent(ua) // 会拼接在默认ua后面
更新小程序
HRiverMini.updateApp(appId)
预置小程序
在工程的
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
的 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
}
}
自定义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 包括
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。