HarmonyOS 「校园二手交易商城」App应用实战 41 直播页整体架构:LiveBroadCastPage
41 直播页整体架构:LiveBroadCastPage
系列五:直播与画中画 · 第 1 篇
对应工程:
products/default/src/main/ets/pages/LiveBroadCastPage.ets与products/pc/src/main/ets/pages/LiveBroadCastPage.ets
引言
系列四我们走完了"列表 → 详情 → 支付"的静态购物链路,从本篇开始进入系列五「直播与画中画」,也是整个项目技术含金量最高的部分:视频播放(AVPlayer)、Surface 渲染(XComponent)与画中画(PiPWindow)三大能力在直播页交汇。LiveBroadCastPage 是这条链路的"总装车间"——它决定页面由哪些区块组成、如何随断点切换手机/PC 布局、如何进入与退出、以及资源如何释放。本项目把它同时放在了 products/default 与 products/pc 两套产品源码中,是「一次开发多端部署」中"产品层差异化"的典型样本。
页面组成:视频、评论、商品与头部
LiveBroadCastPage 本身不实现具体业务,它通过组合特性模块 multishoppinglive 的组件搭出完整直播页:
LiveContent:视频渲染区,内部是 XComponent + AVPlayer(系列五第 2、3 篇)。LiveComment:滚动评论流,叠加在视频区左下角(第 5 篇)。LiveShopList:带货商品列表,横条或侧栏两种形态(第 6 篇)。LiveHeader/LiveHeaderPC:顶部主播信息栏(第 7 篇)。LiveUtil:底部评论输入 + 分享/点赞/购物袋入口(第 8 篇联动购物袋)。
LiveBroadCast(直播舞台容器)中用 Stack 叠加:Stack() {
LiveContent({ widthBp: this.widthBp }) // 底层:视频
.width('100%').height('100%')
Column() { // 中层:评论 + 横向商品条
LiveComment({ widthBp: this.widthBp })
LiveShopList({ isSideListLayout: false, ... })
}
Row() { // 顶层:侧栏商品列表(大屏)
LiveShopList({ isSideListLayout: true, ... })
}
.visibility(/* 非小屏断点才显示 */)
}Stack 是直播页的灵魂:视频永远在最底层,评论、商品、头部都是"悬浮"在画面之上的半透明层,这正是直播间 UI 的通用范式。
进入方式:Navigation 与 navDestination
直播页通过全局路由栈进入。Index.ets 中注册了导航容器与页面映射:
private static readonly liveNavigationId: string = 'navid';
@Builder
PageMap(name: string) {
if (name === 'RootTabsPage') {
RootTabsPage()
} else if (name === 'ProductDetail') {
ProductDetail()
} else if (name === 'LiveBroadCastPage') {
LiveBroadCastPage()
}
}
build() {
HdsNavigation(this.mainPageMap) { RootTabsPage() }
.navDestination(this.PageMap)
.mode(NavigationMode.Stack)
.id(Index.liveNavigationId)
}注意两个"同名异义"的 id:
Index中的liveNavigationId = 'navid'是Navigation容器自己的组件 id。LiveBroadCastPage中LIVE_NAVIGATION_ID = 'navid'同样挂在HdsNavDestination/NavDestination上:.id(LiveBroadCastPage.LIVE_NAVIGATION_ID)。
第 4 篇会看到,这个 navid 是 PiP 创建 navigationId 时的关键入参——画中画需要知道"从哪个导航页面离开、回到哪个导航页面",两个 id 保持一致是画中画能正确回栈的前提。
页面内部通过 @Consumer('mainPageMap') 拿到全局共享的路由栈:
@Consumer('mainPageMap') public mainPageMap: NavPathStack = new NavPathStack();
private onLiveClose(): void {
this.mainPageMap.pop();
}PC 与默认产品的差异组合
两套 LiveBroadCastPage 源码的差异集中在三点:
| 差异点 | products/default | products/pc |
| 侧栏模式(MD/LG/XL)头部 | 一律 LiveHeader | 使用 LiveHeaderPC |
| 侧栏宽度 | 固定 '50%' | 按断点 '50%'/'37.5%' |
| onLiveClose | 进入画中画(startPip 后 pop) | 直接 pop |
两版共同的 isSideMode() 决定走"左右分栏"还是"上下全屏":
private isSideMode(): boolean {
const bp = this.breakpoint.widthBreakpoint;
return bp === WidthBreakpoint.WIDTH_MD || bp === WidthBreakpoint.WIDTH_LG || bp === WidthBreakpoint.WIDTH_XL;
}大屏时 Row 左右排布:左侧直播舞台(宽 62.5%),右侧 sidePanel(37.5%,购物袋/支付卡);小屏时 Column 上下排布,并用 bindSheet 弹出 620vp 的半屏面板:
.bindSheet($$this.isDialogOpen, this.panelContent(), {
height: '620vp',
preferType: SheetType.CENTER,
dragBar: false,
enableOutsideInteractive: false,
showClose: false,
backgroundColor: $r('app.color.surface_secondary')
})getLiveRenderWidthBp() 还有一个细节:打开侧栏后,直播舞台实际渲染宽度缩窄,断点要"降档"(SM→SM、LG→MD),否则直播组件会按全屏断点排版而显得拥挤:
private getLiveRenderWidthBp(): WidthBreakpoint {
if (this.isDialogOpen && this.isSideMode()) {
return new WidthBreakpointType(WidthBreakpoint.WIDTH_SM, WidthBreakpoint.WIDTH_SM,
WidthBreakpoint.WIDTH_MD, WidthBreakpoint.WIDTH_MD).getValue(this.breakpoint.widthBreakpoint);
}
return this.breakpoint.widthBreakpoint;
}此外,页面用 deviceInfo.distributionOSApiVersion >= 60100 做运行时降级:高版本用 HdsNavDestination(UIDesignKit),低版本退回原生 NavDestination,保证同一套页面在不同系统版本上都能运行。
生命周期与资源释放
直播页的资源释放是最容易踩坑的地方,本项目做了两层防护:
- 视频层:
LiveContent.aboutToDisappear中调用avPlayerUtil.release(),把 AVPlayer 的 error/stateChange 回调注销并release()掉。 - 画中画层:
LiveBroadCastPage.aboutToDisappear中调用pipWindowUtil.stopPip(),主动结束 PiP 并清理控制器回调:
aboutToDisappear(): void {
if (this.pipWindowUtil === undefined) {
return;
}
this.pipWindowUtil.stopPip();
}两层的释放顺序有讲究:PiP 还在显示时若先释放 AVPlayer,画中画窗口会立刻黑屏;所以 PiP 的 stopPip 必须走"先停 PiP、再释放播放器"的链路。第 4 篇的 stopPipChain Promise 链就是为此设计的。
注意事项与总结
- navid 一致性:
Index的 Navigation id、LiveBroadCastPage的 NavDestination id、PiP 的navigationId三者必须一致,否则画中画回栈会失效。 - Stack 叠加层级:视频永远在底层,评论/商品/头部为悬浮层;半透明遮罩用
live_header_scrim、live_input_scrim这类带透明度的资源色。 - 断点降档:侧栏打开后直播舞台变窄,必须用
getLiveRenderWidthBp()降档断点,否则商品条、评论宽度会溢出。 - 资源释放:页面
aboutToDisappear释放 PiP,视频组件aboutToDisappear释放 AVPlayer,二者都判空防护(pipWindowUtil === undefined)。
LiveBroadCastPage 作为"总装车间"已经讲清:它组合了视频、评论、商品、头部四大区块,用断点切换手机半屏/PC 侧栏两种布局,并通过 navid 与 PiP 深度绑定。下一篇进入视频渲染的底层:XComponent 与 Surface。
更多推荐



所有评论(0)