📝往期推文全新看点(文中附带最新·鸿蒙全栈学习笔记)

🚩 鸿蒙(HarmonyOS)北向开发知识点记录~

🚩 鸿蒙(OpenHarmony)南向开发保姆级知识点汇总~

🚩 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?

🚩 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~

🚩 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?

🚩 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?

🚩 记录一场鸿蒙开发岗位面试经历~

📃 持续更新中……


概述

本文从目前流行的垂类市场中,选择购物行业应用作为典型案例详细介绍 “一多” 在实际开发中的应用。购物行业应用的核心功能为浏览商品、商品比价和直播购等。根据这些核心功能,本文选择首页、商品分类页、商品详情页、商品支付页、咨询客服页、直播间页等作为典型页面进行开发,遵从多设备的“差异性”、“一致性”、“灵活性”和“兼容性”,能够让开发者快速高效地掌握“一多”能力并实现购物比价应用的相关功能。

购物类应用为了向用户展示更多的商品选择,对垂类内的核心功能进行了独特设计:

  • 商品分类页 主要用于快速查找目标商品,采用分栏的布局提升查找效率。

  • 商品支付页,为避免大面积页面跳转和遮挡商品的关键信息,采用浅层窗口-半模态的方式进行支付。

  • 在查看 商品详情 或 直播 时,通过侧边面板显示其他的辅助信息,增加浏览效率。

  • 直播间页 推荐的商品信息,在多端基于设备屏幕尺寸进行响应式适配,避让直播的关键信息。

  • 退出直播间页时,能够使用画中画小窗口观看直播。

当前系统的产品形态主要有手机、折叠屏、平板和2in1四种,下文的具体实践也将围绕这几种产品形态展开,同时将分别从UX设计、架构设计、页面开发三个角度给出符合“一多”的参考样例,介绍“一多”购物比价应用在开发过程中的最佳实践。

  • UX设计 章节介绍购物比价应用的交互逻辑和通用的设计要点,对于类似的设计要点,开发者可以直接拿来使用。
  • 架构设计 章节推荐“一多”应用使用目录结构更加清晰的三层架构。
  • 页面开发 章节会将页面划分为不同区域,按照区域的开发顺序,介绍如何使用自适应布局和响应式布局实现不同的UI效果。

架构设计

HarmonyOS的分层架构主要包括三个层次:产品定制层、基础特性层和公共能力层,为开发者构建了一个清晰、高效、可扩展的设计架构。

页面开发

本章介绍购物比价应用中如何使用“一多”的布局能力,完成页面层级的一套页面、多端适配。下文将从不同页面展开,介绍每个页面区域使用到具体的布局能力,帮助开发者从0到1进行购物比价应用的开发。

首页

首页通常有入口图标和商品卡片等丰富的商品信息,帮助解决用户浏览及挑选商品的核心需求。观察首页在不同设备上的UX设计图,可以进行如下设计:

  • 将首页划分为7个区域,效果图如下:
sm md lg
效果图
sm
  • 首页区域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个区域,效果图如下:
sm md lg
效果图

商品分类页的4个基础区域介绍及实现方案如下表所示:

区域编号 简介 实现方案
1 顶部搜索框 在sm断点下占满行宽,在md、lg断点下设置justifyContent属性为End。
2 侧边导航 Navigation组件 实现,设置mode属性为Split分栏显示,使用navBarWidthRange约束不同断点下的固定导航栏宽度。
3 广告卡片 Swiper组件设置display在不同断点下为1、2、3,在md断点下设置nextMargin露出后边距,实现自适应布局的占比能力,代码可参考 一多开发实例(长视频)。
4 商品小图 使用 List组件+ 栅格布局 实现每行显示固定个数的商品图,代码可参考 一多开发实例(长视频)
  • 侧边导航

使用Navigation组件实现分栏显示,设置mode为NavigationMode.Split双栏显示,同时设置不同断点下导航栏的最小和最大宽度一致,约束固定的导航栏宽度。

// features/home/src/main/ets/view/ClassifyContent.ets
Navigation(this.pageInfo) {
  ...
}
// 设置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个区域,效果图如下:
sm md lg
效果图

购物袋页的4个基础区域介绍及实现方案如下表所示:

区域编号 简介 实现方案
1 顶部标题栏 剩余空间全部分配给中间空白区,用 Blank组件 实现自适应布局拉伸能力,同 首页顶部页签及搜索框
2 购物袋商品 List组件 实现。
3 结算工具栏 剩余空间全部分配给中间空白区,用Blank组件实现自适应布局拉伸能力,同顶部标题栏。
4 优惠明细 购物袋主区域与优惠明细辅助区域在Row组件中呈左右布局,sm和md断点下只显示购物袋主区域、隐藏优惠明细区域,lg断点下全部显示,代码可参考 一多开发实例(长视频)

商品详情页

商品详情页展示商品大图及详细信息。观察商品详情页在不同设备上的UX设计图,可以进行如下设计:

  • 将商品详情页划分为4个区域,效果图如下:
sm md lg
效果图

商品详情页的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): 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
设计能力点
侧边面板-咨询客服
侧边面板-购物袋
  • 观察商品详情侧边面板的设计,在sm断点下只显示侧边辅助面板,在md和lg断点下使用Row组件呈左右布局,设置layoutWeight属性实现自适应布局的占比能力。在md断点时商品详情与侧边面板宽度为1:1,在lg断点时为5:3。

  • 观察商品详情侧边面板的设计,在sm断点下只显示侧边辅助面板,在md和lg断点下使用Row组件呈左右布局,设置layoutWeight属性实现自适应布局的占比能力。在md断点时商品详情与侧边面板宽度为1:1,在lg断点时为5:3。

// features/detail/src/main/ets/views/ProductMoreDetail.ets
Row() {
  Column() {
    ...
  }
  // 设置商品详情与侧边面板宽度比
  .layoutWeight(new BreakpointType(0, CommonConstants.THREE, CommonConstants.FIVE)
    .getValue(this.currentBreakpoint))
  // 在sm断点下隐藏商品详情页
  .visibility(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_SM ? Visibility.None : Visibility.Visible)

  Column() {
    // 判断侧边面板的辅助信息页面
    if (this.isShoppingBag) {
      DetailShoppingBagView({ isMoreDetail: this.isMoreDetail })
    }
    if (this.isCustomerService) {
      CustomerServiceView()
    }
  }
  // 设置商品详情与侧边面板宽度比
  .layoutWeight(3)
}

商品支付页

商品支付页采用浅层窗口展示商品支付信息。观察商品支付页在不同设备上的UX设计图,效果图如下:

sm md lg
设计能力点
效果图

商品支付页的浅层窗口,在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) {
      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
// 绑定到自定义弹窗控制器
private dialogController: CustomDialogController | null = new CustomDialogController({
  builder: PayCardDialog(),
  customStyle: true
});

// 构建自定义弹窗页面
@CustomDialog
struct PayCardDialog {
  ...
  build() {
    Column() {
      PayCard({...})
    }
  }
}

直播间页

直播画面和推荐的商品信息,在多端基于设备屏幕尺寸进行响应式适配。观察直播间页在不同设备上的UX设计图,可以进行如下设计:

sm md lg
设计能力点
效果图

直播间页的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({...}) {
  ...
}
// 设置List组件的排列方向,sm和md断点下水平,lg断点下垂直
.listDirection(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? Axis.Vertical :
  Axis.Horizontal)

直播侧边面板页

在看直播时,经常需要一边听商品讲解一边浏览商品信息,可利用侧边辅助面板查看商品详情、口袋宝贝或支付页面。直播侧边面板页在不同设备上的UX设计图如下:

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

社区规范:仅讨论OpenHarmony相关问题。

更多推荐