【代码案例】HarmonyOS NEXT 视频卡片和列表区域的联动滚动
HarmonyOS Next应用开发案例(持续更新中……)
本案例完整代码,请访问:https://gitee.com//harmonyos-cases/cases/blob/master/CommonAppDevelopment/feature/videolinkagelist
本案例已上架HarmonyOS NEXT开源组件市场,如需获取或移植该案例,可安装此插件。开发者可使用插件获取鸿蒙组件,添加到业务代码中直接编译运行。
介绍
本示例使用Scroll和List组件嵌套,通过List组件的滚动控制器和nestedScroll属性实现了视频卡片和列表区域的联动滚动场景。
效果图预览
使用说明
- 向上滑动列表,页面向上滚动到末尾后隐藏视频,继续向上滑动,卡片吸顶,列表开始滚动,列表滚动到底触发回弹效果。
- 向下滑动列表,列表先滚动到头部后,页面向下滚动,视频显示,继续向下滑动到页面头部,页面上方触发回弹效果。
- 点击视频卡片中的播放按钮切换视频播放状态。
- 视频卡片点击上一条、下一条时,通过List的滚动控制器控制列表滚动到指定位置,视频卡片不发生滚动。
- 点击列表项,列表发生滚动,视频卡片不滚动。
实现思路
- 初始化新闻列表数据 NEWS_LIST_DATA,通过状态变量currentPlayNews和currentIndex跟踪当前播放的新闻。源码参考VideoLinkageList.ets
// 当前播放的新闻
@State currentPlayNews: NewsItem = new NewsItem('', '');
// 当前播放的新闻在列表中的下标
@State currentIndex: number = 0;
aboutToAppear() {
// 新闻列表数据初始化
NEWS_LIST_DATA.forEach((news: NewsItem) => {
this.newsList.pushData(news);
})
this.currentPlayNews = this.newsList.getData(this.currentIndex);
...
}
- 为了解决新闻列表与外层Scroll容器嵌套时的滚动冲突问题,给新闻列表List设置 nestedScroll 属性,指定列表向末尾端和起始端滚动时与外层Scroll的嵌套滚动方式。源码参考VideoLinkageList.ets
List({ scroller: this.scroller }) {
...
}
.nestedScroll({
scrollForward: NestedScrollMode.PARENT_FIRST, // 可滚动组件往末尾端滚动时的嵌套滚动选项,父组件先滚动,父组件滚动到边缘以后自身滚动。
scrollBackward: NestedScrollMode.SELF_FIRST // 可滚动组件往起始端滚动时的嵌套滚动选项,自身先滚动,自身滚动到边缘以后父组件滚动。
})
- 为了实现视频卡片的吸顶效果,需要根据 Scroll 容器高度和隐藏视频后视频卡片的高度计算新闻列表的高度,使 Scroll 滚动到尾部边缘时,视频隐藏,视频卡片吸顶,新闻列表 List 完全显示。源码参考VideoLinkageList.ets
- 计算Scroll容器高度
async computeScrollHeight() {
// 获取屏幕宽度
let displayHeight: number = px2vp(display.getDefaultDisplaySync().height);
// 获取应用窗口
let appWindow: window.Window = await window.getLastWindow(getContext());
// 获取系统默认区域,一般包含状态栏、导航栏
let systemDefaultArea: window.AvoidArea = appWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
// 获取导航条区域
let navigationIndicatorArea: window.AvoidArea =
appWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
// 获取系统默认区域高度
let systemDefaultHeight: number = px2vp(systemDefaultArea.topRect.height + systemDefaultArea.bottomRect.height);
// 获取系统默认区域高度
let navigationIndicatorHeight: number = px2vp(navigationIndicatorArea.bottomRect.height);
// tabs高度
let tabsHeight: number = displayHeight - systemDefaultHeight - navigationIndicatorHeight - Constants.NAVDESTINATION_PADDING_BOTTOM;
this.scrollHeight = tabsHeight - Constants.TITLE_HEIGHT - Constants.TAB_BAR_HEIGHT - Constants.STROKE_WIDTH;
}
- 计算隐藏视频后视频卡片的高度
// 新闻标题高度
const newsNameHeight = newValue.height as number;
// 计算视频卡片上下外边距的和
const videoCardVerticalMargin = Constants.VIDEO_CARD_MARGIN_TOP + Constants.VIDEO_CARD_MARGIN_BOTTOM;
// 隐藏视频后视频卡片的高度
const videoCardHeight = newsNameHeight + Constants.VIDEO_CONTROL_HEIGHT;
- 计算新闻列表高度
/**
* 计算新闻列表高度, 计算方式: 滚动容器高度 - 隐藏视频后视频卡片的高度 - 视频卡片上下外边距
* 此时外层Scroll滚动到尾部边缘时,视频刚好隐藏,新闻列表List完全显示,继续向上滑动新闻列表自身开始滚动
*/
this.newsListHeight = this.scrollHeight - videoCardHeight - videoCardVerticalMargin;
- 由于视频卡片中不同新闻标题的行数可能不同,因此使用onAreaChange事件监听新闻标题高度变化,动态计算新闻列表高度,当标题的行数变化时改变列表的高度,保证滚动效果不发生改变。源码参考VideoLinkageList.ets
// 新闻标题
Text(this.currentPlayNews.newsName)
.width($r('app.string.video_linkage_list_full_size'))
.fontSize($r('app.string.ohos_id_text_size_headline'))
.fontWeight(FontWeight.Bolder)
.padding($r('app.integer.video_linkage_list_news_name_padding'))
// TODO: 性能知识点:onAreaChange属于频繁回调接口,应该避免在内部进行冗余和耗时操作,例如避免打印日志
.onAreaChange((oldValue: Area, newValue: Area) => {
// 切换新闻后获取新闻标题高度,如果标题高度发生变化后,重新计算新闻列表高度
if (oldValue.height !== newValue.height) {
// 新闻标题高度
const newsNameHeight = newValue.height as number;
// 计算视频卡片上下外边距的和
const videoCardVerticalMargin = Constants.VIDEO_CARD_MARGIN_TOP + Constants.VIDEO_CARD_MARGIN_BOTTOM;
// 隐藏视频后视频卡片的高度
const videoCardHeight = newsNameHeight + Constants.VIDEO_CONTROL_HEIGHT;
/**
* 计算新闻列表高度, 计算方式: 滚动容器高度 - 隐藏视频后视频卡片的高度 - 视频卡片上下外边距
* 此时外层Scroll滚动到尾部边缘时,视频刚好隐藏,新闻列表List完全显示,继续向上滑动新闻列表自身开始滚动
*/
this.newsListHeight = this.scrollHeight - videoCardHeight - videoCardVerticalMargin;
}
})
- 通过状态变量isHideVideo修改视频的高度实现显隐,Scroll滚动到末尾时隐藏视频,视频已隐藏情况下, Scroll向下滚动时显示视频。源码参考VideoLinkageList.ets
// 是否隐藏视频区域
@State @Watch('onIsHideVideoChange') isHideVideo: boolean = false;
Scroll(this.scroller) {
...
}
// TODO: 性能知识点:onScroll属于频繁回调接口,应该避免在内部进行冗余和耗时操作,例如避免打印日志
.onScroll((xOffset: number, yOffset: number) => {
// 视频已隐藏情况下, Scroll向下滚动时显示视频
if (yOffset < 0 && this.isHideVideo) {
this.isHideVideo = false;
}
})
.onReachEnd(() => {
// Scroll滚动到末尾时隐藏视频
this.isHideVideo = true;
})
Stack({ alignContent: Alignment.Bottom }) {
...
}
.width($r('app.string.video_linkage_list_full_size'))
// 修改视频的高度实现显隐控制
.height(this.isHideVideo ? 0 : Constants.VIDEO_HEIGHT)
- 在状态变量isHideVideo的监听回调中,根据视频的显隐状态修改视频卡片的上边距保持Scroll内容高度不变,避免滚动混乱。源码参考VideoLinkageList.ets
// TODO:知识点:根据视频显隐状态修改边距,使用边距代替video占位,使Scroll容器内容高度不变,可以向下滚动显示视频,并且避免滚动混乱
onIsHideVideoChange() {
if (!this.isHideVideo) {
// 视频显示,视频卡片上边距减去视频高度
this.videoMarginTop -= Constants.VIDEO_HEIGHT;
} else {
// 视频隐藏,视频卡片上边距加上视频高度
this.videoMarginTop += Constants.VIDEO_HEIGHT;
}
}
- 在视频卡片中上一条、下一条按钮的点击回调中修改currentIndex和currentPlayNews。源码参考VideoLinkageList.ets
// 上一条
Image($r('app.media.video_linkage_list_play_previous'))
.height($r('app.integer.video_linkage_list_control_previous_next_height'))
.onClick(() => {
// 如果不是第一条,切换至上一条
if (this.currentIndex > 0) {
this.currentIndex--;
this.currentPlayNews = this.newsList.getData(this.currentIndex);
} else {
promptAction.showToast({
message: $r('app.string.video_linkage_list_first_data_toast')
});
}
})
...
// 下一条
Image($r('app.media.video_linkage_list_play_next'))
.height($r('app.integer.video_linkage_list_control_previous_next_height'))
.onClick(() => {
// 如果不是最后一条,切换至下一条
if (this.currentIndex < this.newsList.totalCount() - 1) {
this.currentIndex++;
this.currentPlayNews = this.newsList.getData(this.currentIndex);
} else {
promptAction.showToast({
message: $r('app.string.video_linkage_list_last_data_toast')
});
}
})
- 在新闻列表组件中监听状态变量currentIndex,根据选中项的索引值计算列表的滚动偏移。源码参考VideoLinkageList.ets
// TODO:知识点:监听currentIndex的变化,视频播放卡片切换新闻和点击列表项切换新闻时修改currentIndex,根据下标计算列表的滚动偏移
onCurrentIndexChange() {
// 选中的列表项下标大于3时,列表向上滚动,滚动到与列表显示区域内上方间隔3个列表项或列表尾部时停止。
if (this.currentIndex > Constants.NEWS_LIST_SCROLL_TO_INDEX) {
this.scroller.scrollTo({
yOffset: Constants.NEWS_LIST_ITEM_HEIGHT * (this.currentIndex - Constants.NEWS_LIST_SCROLL_TO_INDEX),
xOffset: 0
});
} else {
// 选中的列表项下标小于等于3时,列表滚动至头部
this.scroller.scrollTo({ yOffset: 0, xOffset: 0 });
}
}
高性能知识点
- 本示例使用了LazyForEach进行数据懒加载,List布局时会根据可视区域按需创建ListItem组件,并在ListItem滑出可视区域外时销毁以降低内存占用。参考文章正确使用LazyForEach优化
工程结构&模块类型
videolinkagelist // har类型
|---/src/main/ets/model
| |---NewsListDataSource.ets // 数据模型层-列表数据模型
| |---NewsItemModel.ets // 数据模型层-列表项数据模型
|---/src/main/ets/pages
| |---VideoLinkageList.ets // 视图层-主页面
|---/src/main/ets/mock
| |---NewsListData.ets // 新闻列表mock数据
|---/src/main/ets/common
| |---Constants.ets // 常量数据
模块依赖
参考资料
1
7
2
浏览1833 编辑于2024-11-05 10:51浙江
全部评论
最多点赞
最新发布
最早发布
写回答
- 为了保障您的信息安全,请勿上传您的敏感个人信息(如您的密码等信息)和您的敏感资产信息(如关键源代码、签名私钥、调试安装包、业务日志等信息),且您需自行承担由此产生的信息泄露等安全风险。
- 如您发布的内容为转载内容,请注明内容来源。
发表
我要发帖子
了解社区公约,与您携手共创和谐专业的开发者社区。