鸿蒙5.0开发实战案例:一多开发实例(长视频)-自适应设计
📑往期推文全新看点(文中附带最新·鸿蒙全栈学习笔记)
✒️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?
✒️ 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~
✒️ 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?
✒️ 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?
✒️ 记录一场鸿蒙开发岗位面试经历~
✒️ 持续更新中……
长视频应用视频详情页提供视频播放、评论互动和提供视频相关信息的功能。观察视频详情页在不同设备上的UX设计图,可以进行如下设计(图中为包括可滑动区域的内容):
- 将视频详情页划分为5个区域,效果图如下:
| 示意图 | sm | md | lg |
|---|---|---|---|
| 效果图 | ![]() |
![]() |
![]() |
- 视频详情页在sm、md和lg断点下有较大差异,在sm和md断点下相关列表、全部评论和写评论区域均显示在视频下方,无视频简介区域;在lg断点下分为侧边栏和主内容区两部分,全部评论和写评论区域显示在右侧边栏,相关列表和视频简介区域显示在主内容区视频下方。
- 视频详情页区域1为视频播放相关组件,占满整个主内容区。
- 视频详情页区域2容器组件尺寸变化时,增加或减小的空间全部分配给标题栏的空白区域;内容按照先后顺序随容器组件尺寸变化显示或隐藏。
- 视频详情页区域3中的图片在sm、md断点下大小不同,在lg断点下随侧边栏尺寸改变大小发生变化。
- 视频详情页区域4的评论框均占满容器组件剩余空间。
- 视频详情页区域5只在lg断点下显示,容器组件尺寸变化时,增加或减小的空间,均匀分配给视频简介的图标、全部分配给周边视频标题的空白区域;选集列表内容按照先后顺序随容器组件尺寸变化显示或隐藏。
长视频应用视频详情页的5个基础区域介绍及实现方案如下表所示:
| 区域编号 | 简介 | 实现方案 |
|---|---|---|
| 1 | 视频播放 | 使用视频播放 AVPlayer 和 XComponent 等相关组件,不详细介绍具体功能实现,读者可以自行查看代码。 |
| 2 | 相关列表 | 标题栏的空白区域使用Blank实现拉伸能力,视频列表通过 List组件 实现延伸能力,同 搜索页搜索发现。 |
| 3 | 全部评论 | 在不同断点下修改评论区图片的宽高和aspectRatio属性实现自适应布局的缩放能力。 |
| 4 | 写评论 | layoutWeight实现自适应布局的拉伸能力,同 首页搜索框。 |
| 5 | 视频简介 | 图标 Row组件 的justifyContent属性设置为FlexAlign.SpaceBetween实现均分能力,选集列表通过 List组件实现延伸能力,周边视频使用layoutWeight实现拉伸能力。 |
在实际开发中,sm和md断点下的设计中,区域1-3和区域4为并列的视频详情页内容,所以对应的开发顺序为区域4,区域1-3;lg断点下的设计中,区域1、2、5和区域3-4是并列的视频详情页内容,所以对应的开发顺序为区域3-4,区域1、2、5。另外,视频详情页设计了额外的功能:边看边评的交互逻辑。
- 全部评论
根据ux设计,全部评论在sm、md断点下显示在视频下方,在lg断点下显示在右侧边栏,实现侧边悬浮面板,使用SideBarContainer组件+断点控制组件显示的位置,同时侧边栏宽度可通过拖拽变宽。全部评论中的图片,在sm、md断点下使用不同的宽高固定值;lg断点下,在SideBarContainer的onAreaChange方法中按侧边栏的宽度变化百分比改变图片宽高,配合图片的aspectRatio属性控制等比放大或缩小,实现自适应布局的缩放能力。
效果图如下:

// features/VideoDetail/src/main/ets/view/VideoDetail.ets
// 侧边栏及主内容区
SideBarContainer() {
Column() {
Scroll() {
// lg断点下的全部评论区
AllComments({ commentImgHeight: $commentImgHeight, commentImgWidth: $commentImgWidth })
.visibility(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? Visibility.Visible :
Visibility.None)
}
SelfComment()
.visibility(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? Visibility.Visible :
Visibility.None)
}
// lg断点下,修改图片宽高
.onAreaChange((newValue: Area) => {
if (newValue.width !== 0) {
let height: number = DetailConstants.COMMENT_IMAGE_MIN_HEIGHT_NUMBER + (Number(newValue.width) -
DetailConstants.SIDE_BAR_MIN_WIDTH_NUMBER) / (this.windowWidth * DetailConstants.COMMENTS_AREA_PERCENT -
DetailConstants.SIDE_BAR_MIN_WIDTH_NUMBER) * (DetailConstants.COMMENT_IMAGE_MAX_HEIGHT_NUMBER -
DetailConstants.COMMENT_IMAGE_MIN_HEIGHT_NUMBER);
let width: number = DetailConstants.COMMENT_IMAGE_MIN_WIDTH_NUMBER + (Number(newValue.width) -
DetailConstants.SIDE_BAR_MIN_WIDTH_NUMBER) / (this.windowWidth * DetailConstants.COMMENTS_AREA_PERCENT -
DetailConstants.SIDE_BAR_MIN_WIDTH_NUMBER) * (DetailConstants.COMMENT_IMAGE_MAX_WIDTH_NUMBER -
DetailConstants.COMMENT_IMAGE_MIN_WIDTH_NUMBER);
this.commentImgHeight = JSON.stringify(height);
this.commentImgWidth = JSON.stringify(width);
}
})
Column() {
// sm、md断点下全部评论区
VideoDetail({ screenHeight: this.screenHeight, avPlayerUtil: this.avPlayerUtil, relatedVideoHeight:
$relatedVideoHeight, videoHeight: $videoHeight })
.layoutWeight(1)
SelfComment()
.visibility(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? Visibility.None :
Visibility.Visible)
}
}
// 控制右侧边栏只在lg断点下显示
.showSideBar(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? true : false)
// 右侧边栏的最小、最大宽度
.minSideBarWidth($r('app.float.side_bar_min_width'))
.maxSideBarWidth(px2vp(this.windowWidth * DetailConstants.COMMENTS_AREA_PERCENT))
// features/VideoDetail/src/main/ets/view/AllComments.ets
// 全部评论区中图片
Image(item.getCommentImageSrc())
// 设置不同断点下的固定值或可变宽高
.width(new BreakpointType(DetailConstants.COMMENT_IMAGE_WIDTH[0], DetailConstants.COMMENT_IMAGE_WIDTH[1],
this.commentImgWidth).getValue(this.currentBreakpoint))
.height(new BreakpointType(DetailConstants.COMMENT_IMAGE_HEIGHT[0],
DetailConstants.COMMENT_IMAGE_HEIGHT[1], this.commentImgHeight).getValue(this.currentBreakpoint))
// 控制lg断点下图片等比放大或缩小,实现缩放能力
.aspectRatio(DetailConstants.COMMENT_IMAGE_ASPECT_RATIO)
- 视频简介
视频简介区域只在lg断点下显示,其中视频简介的图标设置Row组件的justifyContent属性为FlexAlign.SpaceBetween实现均分能力。选集列表使用List组件实现自适应布局的延伸能力,随容器组件尺寸变大,可以看到的集数相应增加,用户也可以通过手指滑动触发列表滑动,查看被隐藏的子节点。周边视频标题栏的空白区域随容器组件尺寸变化而伸缩,使用Blank实现自适应布局的拉伸能力。
效果图如下:
| 示意图 | lg |
|---|---|
| 效果图 | ![]() |
// features/VideoDetail/src/main/ets/view/RelatedList.ets
// 视频简介区域
@Builder
VideoIntroduction() {
Column() {
...
Row() {
ForEach(this.iconList, (item: Resource, index: number) => {
Image(item)
.height($r('app.float.introduction_icon_size'))
.width($r('app.float.introduction_icon_size'))
}, (item: Resource, index: number) => index + JSON.stringify(item))
}
// 图标行实现均分能力
.justifyContent(FlexAlign.SpaceBetween)
...
// 选集列表实现延伸能力
List({ space: CommonConstants.LIST_SPACE }) {
ForEach(this.episodes, (item: string, index: number) => {
ListItem() {
...
}
}, (item: string, index: number) => index + JSON.stringify(item))
}
.scrollBar(BarState.Off)
.listDirection(Axis.Horizontal)
// 周边视频标题栏
this.SubTitleAndMore(DetailConstants.SUB_TITLES[5])
...
}
// 控制只在lg断点下显示
.visibility(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? Visibility.Visible : Visibility.None)
}
@Builder
SubTitleAndMore(subtitle: string) {
Row() {
...
// 空白区域实现拉伸能力
Blank()
...
}
}
- 边看边评的交互逻辑
视频详情页中,不同断点下页面下滑效果不同。在sm和md断点下,下滑时先隐藏相关列表区域,完全隐藏后视频区域等比缩小,缩至最小时固定,改为下滑全部评论区域;上滑时先上滑全部评论区,再等比放大视频区域,最后显示相关列表区域。在lg断点下,下滑时等比缩小视频区域,最小时固定改为下滑全部评论区域;上滑时先上滑全部评论区,再等比放大视频区域。这一效果通过 Scroll组件 的onScrollFrameBegin回调方法中控制相关组件的高度和偏移量实现。当组件高度变化时,返回偏移量为0;当全部评论区滑动时,返回实际偏移量。

// features/videoDetail/src/main/ets/view/RelatedList.ets
build() {
Scroll(this.scroller) {
Column() {
this.RelatedVideoComponent()
this.VideoIntroduction()
AllComments({commentImgHeight: $commentImgHeight, commentImgWidth: $commentImgWidth})
.visibility(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? Visibility.None :
Visibility.Visible)
}
.width(CommonConstants.FULL_PERCENT)
.alignItems(HorizontalAlign.Start)
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.onScrollFrameBegin((offset: number) => {
if (this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG) {
if ((offset > 0) && (this.videoHeight > DetailConstants.MIN_VIDEO_PERCENT)) {
// 视频缩小逻辑
// 滑动占屏幕高度百分比
let offsetPercent = (Math.abs(offset) * DetailConstants.MAX_VIDEO_PERCENT) / this.screenHeight;
// 视频缩小百分比
let heightOffset = offsetPercent < this.videoHeight - DetailConstants.MIN_VIDEO_PERCENT ? offsetPercent :
this.videoHeight - DetailConstants.MIN_VIDEO_PERCENT;
this.videoHeight = this.videoHeight - heightOffset;
// 视频缩小百分比
return { offsetRemain: 0 };
} else if ((offset < 0) && (this.videoHeight < DetailConstants.MAX_VIDEO_PERCENT) &&
(CurrentOffsetUtil.scrollToTop(JSON.stringify(this.scroller.currentOffset())))) {
// 视频缩小百分比
let offsetPercent = (Math.abs(offset) * DetailConstants.MAX_VIDEO_PERCENT) / this.screenHeight;
let heightOffset = offsetPercent < DetailConstants.MAX_VIDEO_PERCENT - this.videoHeight ? offsetPercent :
DetailConstants.MAX_VIDEO_PERCENT - this.videoHeight;
this.videoHeight = this.videoHeight + heightOffset;
// 返回实际偏移量0
return { offsetRemain: 0 };
}
return { offsetRemain: offset };
} else {
if ((offset > 0) && (this.videoHeight === DetailConstants.MAX_VIDEO_PERCENT) && (this.relatedVideoHeight > 0)) {
// 相关列表缩小逻辑
// 滑动占屏幕高度百分比
let offsetPercent = (Math.abs(offset) * DetailConstants.TEN) / this.screenHeight;
this.relatedVideoHeight = (this.relatedVideoHeight - offsetPercent * DetailConstants.RELATED_LIST_HEIGHT) <
0 ? 0 : (this.relatedVideoHeight - offsetPercent * DetailConstants.RELATED_LIST_HEIGHT);
// 返回实际偏移量0
return { offsetRemain: 0 };
} else if ((offset > 0) && (this.videoHeight > DetailConstants.MIN_VIDEO_PERCENT) &&
(this.relatedVideoHeight === 0)) {
// 视频缩小逻辑
// 滑动占屏幕高度百分比
let offsetPercent = (Math.abs(offset) * DetailConstants.MAX_VIDEO_PERCENT) / this.screenHeight;
// 视频缩小百分比
let heightOffset = offsetPercent < this.videoHeight - DetailConstants.MIN_VIDEO_PERCENT ? offsetPercent :
this.videoHeight - DetailConstants.MIN_VIDEO_PERCENT;
this.videoHeight = this.videoHeight - heightOffset;
// 返回实际偏移量0
return { offsetRemain: 0 };
} else if ((offset > 0) && (this.videoHeight === DetailConstants.MIN_VIDEO_PERCENT) &&
(this.relatedVideoHeight === 0)) {
return { offsetRemain: offset };
} else if ((offset < 0) && (this.videoHeight < DetailConstants.MAX_VIDEO_PERCENT) &&
(this.relatedVideoHeight === 0)) {
if (CurrentOffsetUtil.scrollToTop(JSON.stringify(this.scroller.currentOffset()))) {
// 返回实际偏移量0
let offsetPercent = (Math.abs(offset) * DetailConstants.MAX_VIDEO_PERCENT) / this.screenHeight;
let heightOffset = offsetPercent < DetailConstants.MAX_VIDEO_PERCENT - this.videoHeight ? offsetPercent :
DetailConstants.MAX_VIDEO_PERCENT - this.videoHeight;
this.videoHeight = this.videoHeight + heightOffset;
// 返回实际偏移量0
return { offsetRemain: 0 };
}
return { offsetRemain: offset };
} else if ((offset < 0) && (this.videoHeight === DetailConstants.MAX_VIDEO_PERCENT) &&
(this.relatedVideoHeight >= 0)) {
// 相关列表放大逻辑
// 滑动占屏幕高度百分比
let offsetPercent = (Math.abs(offset) * DetailConstants.TEN) / this.screenHeight;
this.relatedVideoHeight = (this.relatedVideoHeight + offsetPercent * DetailConstants.RELATED_LIST_HEIGHT) >
DetailConstants.RELATED_LIST_HEIGHT ? DetailConstants.RELATED_LIST_HEIGHT : (this.relatedVideoHeight +
offsetPercent * DetailConstants.RELATED_LIST_HEIGHT);
// 返回实际偏移量0
return { offsetRemain: 0 };
}
return { offsetRemain: 0 };
}
})
}
- 全屏播放不旋转
在折叠屏展开态这种方形尺寸屏幕的设备上,点击“全屏播放”按钮,视频画面不旋转。在折叠屏展开态下,视频详情页进入全屏播放页不旋转,但是跟随系统传感器方向自动旋转,且受控制中心的旋转开关控制。使用window.setPreferredOrientation(window.Orientation.AUTO_ROTATION_RESTRICTED)设置。首页和视频详情页在其他设备上或折叠屏其他折叠态时竖屏显示,使用window.setPreferredOrientation(window.Orientation.PORTRAIT)设置。
// features/videoDetail/src/main/ets/view/VideoDetail.ets
@Component
export struct VideoDetail {
aboutToAppear() {
this.windowUtil = WindowUtil.getInstance();
if (this.windowUtil === undefined) {
return;
}
// 判断为折叠屏展开态,设置窗口跟随传感器自动旋转,且受旋转开关控制
if (this.currentBreakpoint === BreakpointConstants.BREAKPOINT_MD && display.isFoldable()) {
this.windowUtil.setMainWindowOrientation(window.Orientation.AUTO_ROTATION_RESTRICTED);
}
}
}
// products/phone/src/main/ets/entryability/EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage) {
let windowUtil: WindowUtil | undefined = WindowUtil.getInstance();
if (windowUtil !== undefined) {
windowUtil.setWindowStage(windowStage);
// 设置竖屏显示
windowUtil.setMainWindowPortrait();
}
}

更多推荐







所有评论(0)