Initialization
Initialize HRiverMini after the mPaaS frame is initialized.
HRiverMini.init()To initialize the miniapp, call HRiverMini.init() in the onCreate method of the Ability.
Before you use the SDK, you must initialize the mPaaS frame and set the userId for previews and on-device debugging.
For more information about mPaaS frame initialization, see Initialize mPaaS. The following is an example:
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'
}
}Open a miniapp
Miniapp page routing is based on Navigation. Add the following configuration to the aboutToAppear() method of the Navigation page:
aboutToAppear() {
HRiverMini.notifyNavigationCreate(this.context, this.pageInfos)
}Add 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);
}To start a miniapp, use the following API:
let startParams = new Map<string, Object>() // Startup parameters
HRiverMini.startApp("2023112713520001", startParams)To start a miniapp and navigate to a specific page:
let startParams = new Map<string, Object>()
startParams.set("page", "/page/component/view/view");
HRiverMini.startApp("2020012000000001", startParams)Register a custom API
Call the following API before you initialize HRiverMini.init():
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')
})The import statement is used to dynamically import the custom API's Extension. The implementation of CustomExtension is as follows:
import { ApiBaseExtension,
BridgeCallback,
defineJSAPIClass, ExtensionParameter,
MyExtHubContext,
registerJSAPI, required } from '@alipay/exthub'
@defineJSAPIClass(():ApiBaseExtension => {return new CustomExtension()})
export class CustomExtension extends ApiBaseExtension {
/**
* Custom 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) {
// Read parameters from param.
// Read the context, such as the page context, from context.
// Use the callback to call back to the miniapp.
// Call back specific data after a successful call.
callback.sendSuccessResponse({
data: 'apiSuccess'
})
}
/**
* Custom 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 notification for a failure.
callback.sendErrorResponse(-1, "call failed msg")
}
}Native Notifications Mini Program
import { EngineUtils, HRiverMiniEngine } from '@mpaas/hrivermini'
HRiverMiniEngine.sendToRender(EngineUtils.getPageFromContext(context)?.getRender() || null, 'event', {})Scan a QR code for preview or on-device debugging
Method 1: Start the QR code scanner directly.
let startParams = new Map<string, Object>()
HRiverMini.scan(getContext(this), startParams)Method 2: Use a different scanner and open the scan result.
let startParams = new Map<string, Object>()
HRiverMini.scanByUri(scanResult, startParams) // scanResult is the string of the scan result.Configure the request interval for miniapp packages
mPaaS lets you configure the request interval for miniapp packages. You can configure this setting globally.
HRiverMini.setAsyncReqRate("{\"config\":{\"al\":\"3\",\"pr\":{\"4\":\"86400\",\"common\":\"864000\"},\"ur\":\"1800\",\"fpr\":{\"common\":\"3888000\"}},\"switch\":\"yes\"}")In this example, \"ur\":\"1800\" sets the global update interval. The default value is 1800 seconds. You can change this value to set the global request interval for miniapp packages. The valid range is from 0 to 86400 seconds (0 to 24 hours). A value of 0 indicates that there is no limit on the request interval.
Startup parameters
Key | Value | Description |
query | Example: a=xx&c=xx | Pass parameters to the miniapp. |
page | Example: pages/twoPage/twoPage | Specify the page to open and pass parameters. |
nbupdate | synctry or async. The default is async. |
|
disablePresetMenu | YES or NO. The default is NO. | Specifies whether to hide the capsule menu. |
Set the user agent
HRiverMini.setUserAgent(ua) // Appended to the default user agent.Update a miniapp
HRiverMini.updateApp(appId)Switch configuration
You can use switches to configure features.
HRiverMini.setConfig(`${key}`, `${value}`)The following table describes the keys, values, and their descriptions:
Key | Value | Description |
xriver_show_share_menu | YES or NO. The default is NO. | Specifies whether to show the Share button in the miniapp menu. |
Preset miniapps
In the
rawfiledirectory of your project, create thenebulaPresetandnebulapresetinfodirectories. Place all built-in miniapp packages in thenebulaPresetdirectory. The file name for each package must be itsappid. Place thecustomnebulapreset.jsonfile in thenebulapresetinfodirectory. This file contains the JSON configuration for the built-in miniapps. The JSON format is slightly different from other platforms. You only need to keep the content under the data object.The following is an example:

The content of
customnebulapreset.jsonis as follows:[ { "app_desc":"Miniapp demo", "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":"Miniapp demo 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" }, ]After initialization, call
HRiverMini.loadPresetApp.HRiverMini.loadPresetApp((appId: string) => { // A callback is triggered for each successful installation, returning the appId. hilog.debug(1, "MiniTag", "installPreset: " + appId) })
Miniapp information management
Retrieve miniapp information based on the
appId. The returned information includes theappId,version, andtitle.let result: ESObject = HRiverMini.getAppInfo('1122334455667788') if (result) { // result.appId: The miniapp ID. // result.version: The miniapp version. // result.title: The miniapp name. hilog.debug(1, "MiniTag", `getAppInfo: ${result.appId} ${result.version}`) }Delete miniapp information.
HRiverMini.deleteAppInfo('1122334455667788')Delete all miniapp information.
HRiverMini.deleteAllAppInfo()
Aspect-oriented event interception
You can inject an interception aspect through the registerPoint method of HRiverMini to listen for and intercept partial events.
/**
* Register an interception point.
* extensionName: The name of the implemented Extension.
* pointArr: An array of specific events to intercept.
**/
registerPoint(extensionName: string, pointArr: Array<string>)Back event interception
Event name: CRV_PROTOCOL_XRiverPageBackIntercept
The implementation is as follows:
{ CRV_PROTOCOL_XRiverPageBackIntercept } from '@mpaas/xriverohos';
// PageInterceptExtension is a custom aspect implementation. For more information, see the demo. CRV_PROTOCOL_XRiverPageBackIntercept is the key for the back event.
HRiverMini.registerPoint(PageInterceptExtension.name, [CRV_PROTOCOL_XRiverPageBackIntercept])The implementation of PageInterceptExtension is as follows:
import {
CRV_PROTOCOL_XRiverPageBackIntercept,
defineExtensionConstructor, Extension, ExtensionContext,
Page,
PageBackInterceptPoint } from '@mpaas/xriverohos';
@defineExtensionConstructor((): Extension => {
return new PageInterceptExtension();
})
// Inherit Extension and implement PageBackInterceptPoint.
export class PageInterceptExtension extends Extension implements PageBackInterceptPoint{
constructor() {
super();
// Register the corresponding method name for the CRV_PROTOCOL_XRiverPageBackIntercept interception event. The back event is fixed to interceptBackEvent.
this.registerProtocolFunction(CRV_PROTOCOL_XRiverPageBackIntercept, 'interceptBackEvent');
}
// Implement the interceptBackEvent method. It returns true to intercept or false to not intercept.
// The following method body is a demo example. Whether to intercept depends on your business needs.
interceptBackEvent(context: ExtensionContext): boolean {
let page = context.getCurrentNode() as Page
if (page.isFirstPage()) {
return true
}
return false
}
}Support for hiding the title bar
You can hide the miniapp title bar globally or based on the miniapp ID.
After initialization, add a point interception:
HRiverMini.registerAppPoint(PageInterceptExtension.name, [CRV_PROTOCOL_SESSION_sessionDidStart])The implementation of PageInterceptExtension.ets is as follows:
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()
// To configure based on the appId, you can add a conditional judgment here.
if (originParam) {
originParam['transparent'] = new Boolean(true)
originParam['forceDefaultBackground'] = new Boolean(true)
}
}
}Custom UI
You can customize UI elements, such as the title bar, menu bar, permission dialog box, and miniapp loading and error pages. To do this, set the MYNavigationBarAdapter implementation class during initialization.
TinyAdapterUtils.setProvider(MYNavigationBarAdapter.name, new DemoNavBarAdapter())The implementation of DemoNavBarAdapter.ets is as follows:
import { CRVPage, MYNavigationBarAdapter } from '@mpaas/nebulaintegration';
import { TinyMenuState } from '@mpaas/xriverohos';
import { DemoMenuCustomDialog } from './DemoMenuCustomDialog';
import { DemoNavComponent } from './DemoNavComponent';
export class DemoNavBarAdapter extends MYNavigationBarAdapter {
// Custom menu dialog box UI
getMenuCustomDialog(): WrappedBuilder<[TinyMenuState, CustomDialogController, CRVPage]> {
return wrapBuilder(customDialogBuilder);
}
// Custom title bar UI
getNavBarComponent(): WrappedBuilder<[ESObject]> | undefined {
return wrapBuilder(customNavComponentBuilder);
}
/** Custom loading and error pages
* @param data: {appId: The miniapp ID, appInfo: The loading appInfo (see EntryInfo), loadingProgress: The loading progress, errorCode: The error code, rightButtonState: The button state data}
* @returns
*/
getLoadingComponent(): WrappedBuilder<[ESObject]> | undefined {
return wrapBuilder(customLoadingComponentBuilder);
}
/**
* Custom permission dialog box
* @param component The page component
* @param dlgData {app: The miniapp information, scope: The permission scope (for example, scope.bluetooth), icon: The miniapp logo, title: The miniapp title, desc: The dialog box content (for example, 'Use your Bluetooth')}
* @param reject The callback method for rejection
* @param agree The callback method for agreement
* @returns true: Use a custom dialog box. false: Use the default dialog box.
*/
showPermissionDialog(component: Object, dlgData: ESObject, reject: Function, agree: Function): boolean {
AUPanelManager.showPermissionFrom(component, {
icon: dlgData.icon,
title: dlgData.title,
subTitle: 'Request',
content: dlgData.desc,
subContent: '',
checkbox: undefined,
buttons: [
{
title: 'Deny', type: DialogButtonType.Cancel, action: (isChecked: boolean) => {
reject()
}
},
{
title: 'Allow', type: DialogButtonType.Normal, action: () => {
agree()
}
}
],
info: new ObservedPermissionInfo()
})
return true;
}
}
@Builder
export function customDialogBuilder(state: TinyMenuState, controller: CustomDialogController, page: CRVPage) {
// Menu dialog box UI implementation
DemoMenuCustomDialog({
tinyMenuState: state, // Menu data
customDialogController: controller, // Custom dialog box controller
page: page // The current miniapp page, from which you can get miniapp data
})
}
@Builder
export function customNavComponentBuilder(data: ESObject) {
// Title bar implementation
DemoNavComponent({
needHideBackButton: data.needHideBackButton, // Specifies whether to show the Back button
navigationBarState: data.navigationBarState, // Specific data for the title bar
page: data.page // The current miniapp page, from which you can get miniapp data
})
}
@Builder
export function customLoadingComponentBuilder(data: ESObject) {
DemoLoadingComponent({
appId: data.appId,
appInfo: data.appInfo,
loadingProgress: data.loadingProgress,
errorCode: data.errorCode,
rightButtonState: data.rightButtonState
})
}Support for a custom title bar
The implementation of DemoNavComponent.ets is as follows:
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 // Current page
needHideBackButton: boolean = false; // Specifies whether to show the Back button
@ObjectLink navigationBarState: NavigationBarState // Title bar data
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) => {
// Back button click
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) => {
// Close button
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) => {
// Home button click
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 => {
// Title click
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) => {
// More menu click
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) => {
// Close button click
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
})
}
}Support for a custom menu
The implementation of DemoMenuCustomDialog.ets is as follows:
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('Cancel')
.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
}
}
Support for custom loading and error pages
The following example shows the implementation of DemoLoadingComponent.ets:
import { EntryInfo, RightButtonState } from '@mpaas/xriverohos';
type AnimationStep = () => void;
@Component
export struct DemoLoadingComponent {
@State rightButtonState: RightButtonState = new RightButtonState();
appId?: string = '' // Miniapp ID
@Prop appInfo?: EntryInfo; // Loading information
@Prop loadingProgress: number = 0; // Loading progress
@Prop errorCode: number = 0; // Error code. A non-zero value indicates an error.
@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('Poor network connection').fontColor(0x333333).fontSize(20).width('100%').textAlign(TextAlign.Center)
Row().height(10)
Text('Please try again later').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: The type of permission being requested. For specific values, see the subsequent part of this document. close: The callback method, which needs to be called after the dialog box is canceled. goToSettings parameter: true indicates to go to the Settings page and then dismiss the dialog box; false indicates to dismiss the dialog box directly.
showGuideDialog(type: PermissionType, close: (goToSettings: boolean) => void): boolean {
// Custom dialog box logic. The following is just an example.
AUDialogManager.showNotice({
onClose: () => {
close(false) // Callback
},
title: 'Permission Request ' + type, // Title, based on business requirements
message: '', // Dialog box content
buttons: [{
title: $r('app.string.go_to_settings'),
action: () => {
close(true) // Callback
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('Go to the settings authorization page');
})
}
}]
})
return true
}
}PermissionType types:
export declare enum PermissionType {
None = 0,
Location = 1, // Location
Location_Background = 2,
Location_Approximately = 3,
Camera = 4, // Camera
MicroPhone = 5, // Microphone
BlueTooth = 6, // Bluetooth
Health_Read = 7, // Health data
Notification = 8, // Notification
Pasteboard = 9, // Clipboard
Calendar = 10, // Calendar
Media = 11, // Media
Photo_Write = 12, // Photo album write
Photo = 13, // Photo album read
Contacts = 14, // Address book
Carrier = 15, // Carrier
Vibrate = 16 // Vibrate
}Custom permission guide dialog box
If a system permission, such as location access, is denied, a guide dialog box appears the next time the corresponding feature is called. You can customize this guide dialog box.
After startup and initialization, you can set a custom provider.
import { PermissionAdapter, PermissionAdapterUtils } from '@mpaas/mpaas_permission_fortress';
...
...
PermissionAdapterUtils.setProvider(PermissionAdapter.name, new DemoPermissionAdapter())The implementation of DemoPermissionAdapter.ets is as follows:
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: The type of permission being requested. For specific values, see the subsequent part of this document. close: The callback method, which needs to be called after the dialog box is canceled. goToSettings parameter: true indicates to go to the Settings page and then dismiss the dialog box; false indicates to dismiss the dialog box directly.
showGuideDialog(type: PermissionType, close: (goToSettings: boolean) => void): boolean {
// Custom dialog box logic. The following is just an example.
AUDialogManager.showNotice({
onClose: () => {
close(false) // Callback
},
title: 'Permission Request ' + type, // Title, based on business requirements
message: '', // Dialog box content
buttons: [{
title: $r('app.string.go_to_settings'),
action: () => {
close(true) // Callback
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('Go to the settings authorization page');
})
}
}]
})
return true
}
}PermissionType types:
export declare enum PermissionType {
None = 0,
Location = 1, // Location
Location_Background = 2,
Location_Approximately = 3,
Camera = 4, // Camera
MicroPhone = 5, // Microphone
BlueTooth = 6, // Bluetooth
Health_Read = 7, // Health data
Notification = 8, // Notification
Pasteboard = 9, // Clipboard
Calendar = 10, // Calendar
Media = 11, // Media
Photo_Write = 12, // Photo album write
Photo = 13, // Photo album read
Contacts = 14, // Address book
Carrier = 15, // Carrier
Vibrate = 16 // Vibrate
}Support for delayed initialization
To comply with privacy policy requirements, you must support delayed initialization. For delayed initialization, you need to inject the homepage UIAbility information during the initialization process. The procedure is as follows:
Create the
HomeAbilityContextclass.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; } }In the
onCreatemethod of the entryEntryAbilityStage, registerabilityLifecycleand cacheTopUIAbility.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)After you initialize the miniapp, you must inject the homepage context before you call
HRiverMini.notifyNavigationCreate.let applicationManager: ESObject = Framework.microApplicationContext["applicationManger"] if (applicationManager) { applicationManager["mTopAbility"] = getTopAbility() // Get the TopUIAbility cached in the second step. 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) } }
Special notes for components in the current version
The map component is based on Huawei Map and supports the
getCurrentLocationandmoveToLocationAPIs. You must apply for permission to use it on the official Huawei Map website.To retrieve clipboard content, the app must request the
ohos.permission.READ_PASTEBOARDpermission. This is an Access Control List (ACL) permission.
Unsupported components and APIs in the current version
File APIs: Retrieve file information, retrieve file list, remove file, delete file
canvas2
Live streaming
Contacts
Hide keyboard
Map component support
Supported map component APIs
latitude
longitude
scale
markers
polyline
circles
polygon
include-points
Unsupported map component APIs
Advanced custom rendering for maps: The customCallout of a marker only supports type=2.
The style attribute only supports style rendering of type=3. Other types are not supported.
The onRegionChange event only supports the end type, not the start type.