页面开发

本章介绍购物比价应用中如何使用“一多”的布局能力,完成页面层级的一套页面、多端适配。下文将从不同页面展开,介绍每个页面区域使用到具体的布局能力,帮助开发者从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属性实现自适应布局的占比能力,同[商品详情侧边面板页]。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐