鸿蒙开发5.0案例【一多开发(购物比价app)】
本章介绍购物比价应用中如何使用“一多”的布局能力,完成页面层级的一套页面、多端适配。下文将从不同页面展开,介绍每个页面区域使用到具体的布局能力,帮助开发者从0到1进行购物比价应用的开发。
页面开发
本章介绍购物比价应用中如何使用“一多”的布局能力,完成页面层级的一套页面、多端适配。下文将从不同页面展开,介绍每个页面区域使用到具体的布局能力,帮助开发者从0到1进行购物比价应用的开发。
首页
首页通常有入口图标和商品卡片等丰富的商品信息,帮助解决用户浏览及挑选商品的核心需求。观察首页在不同设备上的UX设计图,可以进行如下设计:
- 将首页划分为7个区域,效果图如下:

-
首页区域2在小设备上呈两行显示,在中设备和大设备上单行显示,断点变化时切换显示效果。
-
首页区域3使用自适应布局延伸能力随不同设备尺寸延伸或隐藏,区域4、5使用自适应布局占比能力和均分能力。
-
首页区域1、5-8使用响应式布局中的栅格断点系统,根据断点变化改变组件内属性,从而实现相应的布局效果。
| 区域编号 | 简介 | 实现方案 |
|---|---|---|
| 1 | 底部/侧边页签 | 借助[栅格布局]监听断点变化改变位置。 |
| 2 | 顶部页签及搜索框 | 栅格布局监听断点变化实现折行显示,[List组件]实现延伸能力,layoutWeight实现拉伸能力。 |
| 3 | 商品分类图标 | List组件实现延伸能力。 |
| 4 | 商品卡片 | [Swiper组件],指定displayCount属性实现占比能力,设置aspectRatio属性实现缩放能力。 |
| 5 | 福利专区 | [Row组件]的justifyContent属性设置为FlexAlign.SpaceBetween实现均分能力。 |
| 6 | 甄选推荐 | 响应式布局的栅格布局,设置aspectRatio属性实现缩放能力。 |
| 7 | 限时秒杀 | 响应式布局的栅格布局,设置aspectRatio属性实现缩放能力,同甄选推荐。 |
商品分类页
商品分类页主要用于快速查找目标商品。观察商品分类页在不同设备上的UX设计图,可以进行如下设计:
- 将商品分类页划分为4个区域,效果图如下:

商品分类页的4个基础区域介绍及实现方案如下表所示:
| 区域编号 | 简介 | 实现方案 |
|---|---|---|
| 1 | 顶部搜索框 | 在sm断点下占满行宽,在md、lg断点下设置justifyContent属性为End。 |
| 2 | 侧边导航 | [Navigation组件]实现,设置mode属性为Split分栏显示,使用navBarWidthRange约束不同断点下的固定导航栏宽度。 |
| 3 | 广告卡片 | [Swiper组件]设置displayCount在不同断点下为1、2、3,在md断点下设置nextMargin露出后边距,实现自适应布局的占比能力。 |
| 4 | 商品小图 | 使用[List组件]+[栅格布局]实现每行显示固定个数的商品图。 |
-
侧边导航
使用Navigation组件实现分栏显示,设置mode为NavigationMode.Split双栏显示,同时设置不同断点下导航栏的最小和最大宽度一致,约束固定的导航栏宽度。
// features/home/src/main/ets/view/ClassifyContent.ets
Navigation(this.pageInfo) {
// ...
}
.layoutWeight(1)
// 设置Navigation组件双栏显示
.mode(NavigationMode.Split)
// 初始化导航栏宽度
.navBarWidth(new BreakpointType($r('app.float.classify_navigation_bar_width_sm'),
$r('app.float.classify_navigation_bar_width_md'), $r('app.float.classify_navigation_bar_width_lg'))
.getValue(this.currentBreakpoint))
// 设置不同断点下导航栏的最小宽度与最大宽度一致
.navBarWidthRange([new BreakpointType($r('app.float.classify_navigation_bar_width_sm'),
$r('app.float.classify_navigation_bar_width_md'), $r('app.float.classify_navigation_bar_width_lg'))
.getValue(this.currentBreakpoint), new BreakpointType($r('app.float.classify_navigation_bar_width_sm'),
$r('app.float.classify_navigation_bar_width_md'), $r('app.float.classify_navigation_bar_width_lg'))
.getValue(this.currentBreakpoint)])
购物袋页
购物袋页通常用于快速查看并支付待购买的商品,在大屏上采用右侧露出辅助信息确保页面的使用效率。观察购物袋页在不同设备上的UX设计图,可以进行如下设计:
- 将购物袋页划分为4个区域,效果图如下:

购物袋页的4个基础区域介绍及实现方案如下表所示:
| 区域编号 | 简介 | 实现方案 |
|---|---|---|
| 1 | 顶部标题栏 | 剩余空间全部分配给中间空白区,用[Blank组件]实现自适应布局拉伸能力,同[首页顶部页签及搜索框]。 |
| 2 | 购物袋商品 | [List组件]实现。 |
| 3 | 结算工具栏 | 剩余空间全部分配给中间空白区,用Blank组件实现自适应布局拉伸能力,同顶部标题栏。 |
| 4 | 优惠明细 | 购物袋主区域与优惠明细辅助区域在Row组件中呈左右布局,sm和md断点下只显示购物袋主区域、隐藏优惠明细区域,lg断点下全部显示。 |
商品详情页
商品详情页展示商品大图及详细信息。观察商品详情页在不同设备上的UX设计图,可以进行如下设计:
- 将商品详情页划分为4个区域,效果图如下:

商品详情页的4个基础区域介绍及实现方案如下表所示:
| 区域编号 | 简介 | 实现方案 |
|---|---|---|
| 1 | 商品大图 | [Swiper组件],指定displayCount属性实现延伸能力,设置aspectRatio属性实现缩放能力。 |
| 2 | 商品详细信息 | 商品大图区域与商品详细信息区域在sm和md断点下使用Column组件呈上下布局,在lg断点下使用Row组件呈左右布局,同[商品详情侧边面板页]。 |
| 3 | 购买工具栏 | 剩余空间按比例分配给加入购物袋与购买按钮,用layoutWeight属性实现自适应布局占比能力,同[首页顶部页签及搜索框]。 |
| 4 | 画中画 | 使用[PiPWindow]实现画中画功能,启动、停止小窗直播及控制视频播放。 |
商品详情页在大屏设备上提供分屏功能,满足同时查看两个商品的详细参数进行购物比价的诉求。分屏通过创建一个新的UIAbility,并设置窗口显示为分屏模式实现。分屏后左右屏幕的宽度为1:1,在折叠屏上的效果图如下:

创建新的UIAbility,需要在phone目录下创建SecondAbility.ets,注册与EntryAbility相同的UIAbility生命周期回调。下一步需要在phone目录的module.json5配置文件,修改abilities属性注册SecondAbility,详情可参考源码。启动分屏时,调用UIAbilityContext的StartAbility接口,设置窗口模式为分屏并启动SecondAbility。关闭分屏时,调用UIAbilityContext的terminateSelf接口。
// features/detail/src/main/ets/views/ProductDetail.ets
Image(this.isSplitMode ? $r('app.media.icon_split') : $r('app.media.ic_mate_pad_2'))
// ...
.onClick(() => {
if (deviceInfo.deviceType === CommonConstants.DEVICE_TYPES[0]) {
return;
}
if (!this.isSplitMode) {
// 设置启动SecondAbility
let want: Want = {
bundleName: 'com.huawei.multishoppingpricecomparison',
abilityName: 'SecondAbility'
};
// 设置分屏的窗口启动模式
let option: StartOptions = { windowMode: AbilityConstant.WindowMode.WINDOW_MODE_SPLIT_PRIMARY };
// 启动分屏
(getContext(this) as common.UIAbilityContext).startAbility(want, option);
} else {
// 关闭分屏
(getContext(this) as common.UIAbilityContext).terminateSelf();
}
})
另外,为了增强在大设备上的浏览效率,用户点击全部评论,页面三分栏展示右侧的全部评价页面,使用SideBarContainer组件实现。
效果图如下:

// features/detail/src/main/ets/views/ProductHome.ets
SideBarContainer() {
// 右侧全部评论
Column() {
Image($r('app.media.icon_close_4'))
// ...
AllComments()
}
.alignItems(HorizontalAlign.End)
.height(CommonConstants.FULL_PERCENT)
.padding({
top: deviceInfo.deviceType === CommonConstants.DEVICE_TYPES[0] ? 0 : this.topRectHeight,
left: $r('app.float.three_column_page_padding'),
right: $r('app.float.three_column_page_padding')
})
// 左侧商品详情
Row() {
// ...
}
// ...
}
// 控制全部评论区是否显示
.showSideBar(this.isShowingSidebar)
.showControlButton(false)
.sideBarPosition(SideBarPosition.End)
.divider({
strokeWidth: $r('app.float.sidebar_divider_width'),
color: ResourceUtil.getCommonDividerColor()
})
// 固定右侧全部评论区宽度
.minSideBarWidth(px2vp(this.windowWidth) / CommonConstants.THREE)
.maxSideBarWidth(px2vp(this.windowWidth) / CommonConstants.THREE)
// 设置全部评论区是否跟随窗口宽度自动隐藏
.autoHide(false)
-
为了方便用户浏览其他页面时能够继续观看直播内容,购物直播设计了额外的画中画功能。点击直播间页的关闭按钮,返回上一页并以小窗模式呈现直播内容。画中画功能的实现分为以下步骤:
使用@ohos.PiPWindow模块的create接口创建画中画控制器,使用startPiP接口启动画中画,启动后返回上一页。其中画中画播放的视频内容需要使用XComponent+AVPlayer组件实现,读者可以自行查看源码。
// commons/base/src/main/ets/utils/PipWindowUtil.ets
async startPip(navId: string, mXComponentController: XComponentController, context: Context, pageInfos: NavPathStack):
Promise<void> {
if (!PiPWindow.isPiPEnabled()) {
Logger.error(`picture in picture disabled for current OS`);
return;
}
let config: PiPWindow.PiPConfiguration = {
context: context,
// 绑定XComponent直播播放组件
componentController: mXComponentController,
// 当前页面的导航ID
navigationId: navId,
// 画中画直播媒体类型
templateType: PiPWindow.PiPTemplateType.VIDEO_LIVE
};
// 创建画中画控制器
let promise : Promise<PiPWindow.PiPController> = PiPWindow.create(config);
await promise.then((controller: PiPWindow.PiPController) => {
this.pipController = controller;
// 初始化画中画控制器
this.initPipController();
// 通过startPip接口开启画中画功能
this.pipController.startPiP().then(() => {
Logger.info(`Succeeded in starting pip.`);
if (this.avPlayerUtil === undefined) {
return;
}
this.avPlayerUtil.play();
pageInfos.pop();
}).catch((err: BusinessError) => {
Logger.error(`Failed to start pip. Cause: ${err.code}, message: ${err.message}`);
});
}).catch((err: BusinessError) => {
Logger.error(`Failed to create pip controller. Cause: ${err.code}, message: ${err.message}`);
});
}
初始化画中画控制器时,分别注册画中画生命周期状态和直播控制事件的监听。
// commons/base/src/main/ets/utils/PipWindowUtil.ets
initPipController(): void {
if (!this.pipController) {
return;
}
// 注册画中画生命周期状态监听
this.pipController.on('stateChange', (state: PiPWindow.PiPState, reason: string) => {
this.onStateChange(state, reason);
});
// 注册直播控制事件监听
this.pipController.on('controlPanelActionEvent', (event: PiPWindow.PiPActionEventType) => {
this.onActionEvent(event);
});
}
使用stopPiP接口关闭画中画。
// commons/base/src/main/ets/utils/PipWindowUtil.ets
// 通过调用stopPip来关闭画中画
async stopPip(): Promise<void> {
if (this.pipController) {
let promise : Promise<void> = this.pipController.stopPiP();
promise.then(() => {
this.isShowingPip = false;
Logger.info(`Succeeded in stopping pip.`);
try {
this.pipController?.off('stateChange');
this.pipController?.off('controlPanelActionEvent');
} catch (exception) {
Logger.error('Failed to unregister callbacks. Code: ' + JSON.stringify(exception));
}
}).catch((err: BusinessError) => {
Logger.error(`Failed to stop pip. Cause: ${err.code}, message: ${err.message}`);
});
}
}
商品详情侧边面板页
在查看商品详情时,经常会有咨询客服或查看购物车的诉求,可采用侧边面板显示客服对话等辅助信息,提升浏览效率,实现边看商品边聊天咨询等体验。
- 侧边面板咨询客服,效果图如下:

- 观察商品详情侧边面板的设计,在sm断点下只显示侧边辅助面板,在md和lg断点下使用Row组件呈左右布局,设置layoutWeight属性实现自适应布局的占比能力。在md断点时商品详情与侧边面板宽度为1:1,在lg断点时为5:3。
// features/detail/src/main/ets/view/ProdutUtilView.ets
Button(DetailConstants.BUTTON_NAMES[1])
// ...
// sm断点下绑定底部半模态页面
.bindSheet($$this.isDialogOpen,
this.PayCardBuilder(), {
height: $r('app.float.pay_bind_sheet_height'),
preferType: SheetType.CENTER,
dragBar: false,
enableOutsideInteractive: true,
onDisappear: () => { this.isDialogOpen = false },
showClose: false,
backgroundColor: $r('app.color.pay_bind_sheet_background')
})
.onClick(() => {
if (this.isLivePage || this.isSplitMode) {
return;
}
if (this.currentBreakpoint === BreakpointConstants.BREAKPOINT_SM) {
// sm断点下打开半模态页面
this.isDialogOpen = true;
} else {
if (this.dialogController === null) {
return;
}
// md和lg断点下弹出自定义弹窗
this.dialogController.open();
this.isDialogOpen = false;
}
})
商品支付页
商品支付页采用浅层窗口展示商品支付信息。观察商品支付页在不同设备上的UX设计图,效果图如下:

商品支付页的浅层窗口,在sm断点下使用bindSheet为购买按钮绑定底部半模态页面,在md和lg断点下使用居中半模态自定义弹窗居中显示。
// features/detail/src/main/ets/view/ProdutUtilView.ets
Button(DetailConstants.BUTTON_NAMES[1])
// ...
// sm断点下绑定底部半模态页面
.bindSheet($$this.isDialogOpen,
this.PayCardBuilder(), {
height: $r('app.float.pay_bind_sheet_height'),
preferType: SheetType.CENTER,
dragBar: false,
enableOutsideInteractive: true,
onDisappear: () => { this.isDialogOpen = false },
showClose: false,
backgroundColor: $r('app.color.pay_bind_sheet_background')
})
.onClick(() => {
if (this.isLivePage || this.isSplitMode) {
return;
}
if (this.currentBreakpoint === BreakpointConstants.BREAKPOINT_SM) {
// sm断点下打开半模态页面
this.isDialogOpen = true;
} else {
if (this.dialogController === null) {
return;
}
// md和lg断点下弹出自定义弹窗
this.dialogController.open();
this.isDialogOpen = false;
}
})
半模态页面使用@Builder注解构建,绑定到bindSheet事件。
// features/detail/src/main/ets/view/ProdutUtilView.ets
// 构建底部半模态页面
@Builder
PayCardBuilder() {
Column() {
PayCard({
// ...
})
}
// ...
}
自定义弹窗使用@CustomerDialog注解构建,绑定到自定义弹窗控制器。
// features/detail/src/main/ets/view/ProdutUtilView.ets
// 构建底部半模态页面
@Builder
PayCardBuilder() {
Column() {
PayCard({
// ...
})
}
// ...
}
// features/detail/src/main/ets/view/ProdutUtilView.ets
// 构建自定义弹窗页面
@CustomDialog
struct PayCardDialog {
// ...
build() {
Column() {
PayCard({
// ...
})
}
// ...
}
}
直播间页
直播画面和推荐的商品信息,在多端基于设备屏幕尺寸进行响应式适配。观察直播间页在不同设备上的UX设计图,可以进行如下设计:

直播间页的3个基础区域介绍及实现方案如下表所示:
| 区域编号 | 简介 | 实现方案 |
|---|---|---|
| 1 | 直播内容 | [Stack组件]控制子组件的显示层级,在sm断点下aspectRatio属性控制直播图片等比放大实现自适应能力的缩放能力,在md和lg断点下固定大小,同[商品详情页商品大图]。 |
| 2 | 直播弹幕及推荐商品 | 使用[Flex组件]+[List组件],在sm和md断点下呈上下结构,显示在下方,在lg断点下呈左右结构,显示在两侧并尾部对齐。 |
| 3 | 发表弹幕 | [TextInput组件]设置layoutWeight实现自适应布局拉伸能力,同[首页顶部页签及搜索框]。 |
-
直播弹幕及推荐商品
Flex组件的direction和justifyContent属性控制子组件在容器主轴上的位置,sm和md断点下在容器底部,lg断点下在容器两侧。List组件控制列表的排列方向,sm和md断点下水平,lg断点下垂直。
// features/detail/src/main/ets/view/LiveMaskLayer.ets
Flex({
// 设置子组件在Flex容器的主轴方向,sm和md断点下垂直,lg断点下水平
direction: this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? FlexDirection.Row :
FlexDirection.Column,
// 设置主轴的对齐格式,sm和md断点下均分,lg断点下尾部对齐
justifyContent: this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? FlexAlign.SpaceBetween :
FlexAlign.End
}) {
Comment({ currentBreakpoint: this.currentBreakpoint })
LiveShopList({
currentBreakpoint: this.currentBreakpoint,
detailType: this.detailType,
isMoreDetail: this.isMoreDetail
})
}
// features/detail/src/main/ets/view/LiveShopList.ets
// 设置List组件的排列方向,sm和md断点下水平,lg断点下垂直
.listDirection(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? Axis.Vertical :
Axis.Horizontal)
直播侧边面板页
在看直播时,经常需要一边听商品讲解一边浏览商品信息,可利用侧边辅助面板查看商品详情、口袋宝贝或支付页面。直播侧边面板页在不同设备上的UX设计图如下:

- 侧边面板-商品详情页,在sm断点下不显示,在md和lg断点下使用Row组件呈左右布局,设置layoutWeight属性实现自适应布局的占比能力,同[商品详情侧边面板页]。在md断点时商品详情与侧边面板宽度为1:1,在lg断点时为5:3。
- 观察直播侧边面板-口袋宝贝页和支付页的设计,在sm断点下使用bindSheet为组件绑定半模态页面,同[商品支付页],在md和lg断点下使用Row组件呈左右布局,设置layoutWeight属性实现自适应布局的占比能力,同[商品详情侧边面板页]。
更多推荐


所有评论(0)