介紹
本示例使用Scroll和List組件嵌套,通過(guò)List組件的滾動(dòng)控制器和nestedScroll屬性實(shí)現(xiàn)了視頻卡片和列表區(qū)域的聯(lián)動(dòng)滾動(dòng)場(chǎng)景。
效果圖預(yù)覽

使用說(shuō)明
- 向上滑動(dòng)列表,頁(yè)面向上滾動(dòng)到末尾后隱藏視頻,繼續(xù)向上滑動(dòng),卡片吸頂,列表開始滾動(dòng),列表滾動(dòng)到底觸發(fā)回彈效果。
- 向下滑動(dòng)列表,列表先滾動(dòng)到頭部后,頁(yè)面向下滾動(dòng),視頻顯示,繼續(xù)向下滑動(dòng)到頁(yè)面頭部,頁(yè)面上方觸發(fā)回彈效果。
- 點(diǎn)擊視頻卡片中的播放按鈕切換視頻播放狀態(tài)。
- 視頻卡片點(diǎn)擊上一條、下一條時(shí),通過(guò)List的滾動(dòng)控制器控制列表滾動(dòng)到指定位置,視頻卡片不發(fā)生滾動(dòng)。
- 點(diǎn)擊列表項(xiàng),列表發(fā)生滾動(dòng),視頻卡片不滾動(dòng)。
實(shí)現(xiàn)思路
- 初始化新聞列表數(shù)據(jù) NEWS_LIST_DATA,通過(guò)狀態(tài)變量currentPlayNews和currentIndex跟蹤當(dāng)前播放的新聞。
// 當(dāng)前播放的新聞
@State currentPlayNews: NewsItem = new NewsItem('', '');
// 當(dāng)前播放的新聞在列表中的下標(biāo)
@State currentIndex: number = 0;
aboutToAppear() {
// 新聞列表數(shù)據(jù)初始化
NEWS_LIST_DATA.forEach((news: NewsItem) => {
this.newsList.pushData(news);
})
this.currentPlayNews = this.newsList.getData(this.currentIndex);
...
}
- 為了解決新聞列表與外層Scroll容器嵌套時(shí)的滾動(dòng)沖突問題,給新聞列表List設(shè)置 nestedScroll 屬性,指定列表向末尾端和起始端滾動(dòng)時(shí)與外層Scroll的嵌套滾動(dòng)方式。
List({ scroller: this.scroller }) {
...
}
.nestedScroll({
scrollForward: NestedScrollMode.PARENT_FIRST, // 可滾動(dòng)組件往末尾端滾動(dòng)時(shí)的嵌套滾動(dòng)選項(xiàng),父組件先滾動(dòng),父組件滾動(dòng)到邊緣以后自身滾動(dòng)。
scrollBackward: NestedScrollMode.SELF_FIRST // 可滾動(dòng)組件往起始端滾動(dòng)時(shí)的嵌套滾動(dòng)選項(xiàng),自身先滾動(dòng),自身滾動(dòng)到邊緣以后父組件滾動(dòng)。
})
- 為了實(shí)現(xiàn)視頻卡片的吸頂效果, Scroll 容器的內(nèi)容高度使用 calc 計(jì)算屬性設(shè)置為 Scroll 容器高度和視頻高度的和,使 Scroll 滾動(dòng)到尾部邊緣時(shí),視頻隱藏,視頻卡片吸頂。
Scroll(this.scroller) {
Column() {
NewsVideoView({
currentPlayNews: this.currentPlayNews,
currentIndex: this.currentIndex,
newsList: this.newsList,
isHideVideo: this.isHideVideo,
scrollHeight: this.scrollHeight,
newsListHeight: this.newsListHeight,
})
.margin({ top: this.videoMarginTop, bottom: $r('app.integer.video_linkage_list_video_card_margin_bottom') })
NewsListView({
currentPlayNews: this.currentPlayNews,
currentIndex: this.currentIndex,
newsList: this.newsList,
isHideVideo: this.isHideVideo,
newsListHeight: this.newsListHeight
})
}
.height(`calc(${Constants.VIDEO_HEIGHT}vp + 100%)`)
}
- 新聞列表組件設(shè)置 layoutWeight 為1,使列表自動(dòng)占滿 Scroll 內(nèi)容的剩余空間,當(dāng)視頻卡片吸頂時(shí)新聞列表可以完全顯示,并且當(dāng)新聞標(biāo)題改變導(dǎo)致卡片高度發(fā)生變化時(shí),新聞列表組件高度也相應(yīng)變化。
List({ scroller: this.scroller }) {
...
}
.layoutWeight(Constants.LAYOUT_WEIGHT)
- 通過(guò)狀態(tài)變量isHideVideo修改視頻的高度實(shí)現(xiàn)顯隱,Scroll滾動(dòng)到末尾時(shí)隱藏視頻,視頻已隱藏情況下, Scroll向下滾動(dòng)時(shí)顯示視頻。
// 是否隱藏視頻區(qū)域
@State @Watch('onIsHideVideoChange') isHideVideo: boolean = false;
Scroll(this.scroller) {
...
}
// TODO: 性能知識(shí)點(diǎn):onScroll屬于頻繁回調(diào)接口,應(yīng)該避免在內(nèi)部進(jìn)行冗余和耗時(shí)操作,例如避免打印日志
.onScroll((xOffset: number, yOffset: number) => {
// 視頻已隱藏情況下, Scroll向下滾動(dòng)時(shí)顯示視頻
if (yOffset < 0 && this.isHideVideo) {
this.isHideVideo = false;
}
})
.onReachEnd(() => {
// Scroll滾動(dòng)到末尾時(shí)隱藏視頻
this.isHideVideo = true;
})
Stack({ alignContent: Alignment.Bottom }) {
...
}
.width($r('app.string.video_linkage_list_full_size'))
// 修改視頻的高度實(shí)現(xiàn)顯隱控制
.height(this.isHideVideo ? 0 : Constants.VIDEO_HEIGHT)
- 在狀態(tài)變量isHideVideo的監(jiān)聽回調(diào)中,根據(jù)視頻的顯隱狀態(tài)修改視頻卡片的上邊距保持Scroll內(nèi)容高度不變,避免滾動(dòng)混亂。
// TODO:知識(shí)點(diǎn):根據(jù)視頻顯隱狀態(tài)修改邊距,使用邊距代替video占位,使Scroll容器內(nèi)容高度不變,可以向下滾動(dòng)顯示視頻,并且避免滾動(dòng)混亂
onIsHideVideoChange() {
if (!this.isHideVideo) {
// 視頻顯示,視頻卡片上邊距減去視頻高度
this.videoMarginTop -= Constants.VIDEO_HEIGHT;
} else {
// 視頻隱藏,視頻卡片上邊距加上視頻高度
this.videoMarginTop += Constants.VIDEO_HEIGHT;
}
}
- 在視頻卡片中上一條、下一條按鈕的點(diǎn)擊回調(diào)中修改currentIndex和currentPlayNews。
// 上一條
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')
});
}
})
- 在新聞列表組件中監(jiān)聽狀態(tài)變量currentIndex,根據(jù)選中項(xiàng)的索引值計(jì)算列表的滾動(dòng)偏移。
// TODO:知識(shí)點(diǎn):監(jiān)聽currentIndex的變化,視頻播放卡片切換新聞和點(diǎn)擊列表項(xiàng)切換新聞時(shí)修改currentIndex,根據(jù)下標(biāo)計(jì)算列表的滾動(dòng)偏移
onCurrentIndexChange() {
// 選中的列表項(xiàng)下標(biāo)大于3時(shí),列表向上滾動(dòng),滾動(dòng)到與列表顯示區(qū)域內(nèi)上方間隔3個(gè)列表項(xiàng)或列表尾部時(shí)停止。
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 {
// 選中的列表項(xiàng)下標(biāo)小于等于3時(shí),列表滾動(dòng)至頭部
this.scroller.scrollTo({ yOffset: 0, xOffset: 0 });
}
}
高性能知識(shí)點(diǎn)
- 本示例使用了LazyForEach進(jìn)行數(shù)據(jù)懶加載,List布局時(shí)會(huì)根據(jù)可視區(qū)域按需創(chuàng)建ListItem組件,并在ListItem滑出可視區(qū)域外時(shí)銷毀以降低內(nèi)存占用。
工程結(jié)構(gòu)&模塊類型
videolinkagelist // har類型
|---/src/main/ets/model
| |---NewsListDataSource.ets // 數(shù)據(jù)模型層-列表數(shù)據(jù)模型
| |---NewsItemModel.ets // 數(shù)據(jù)模型層-列表項(xiàng)數(shù)據(jù)模型
|---/src/main/ets/pages
| |---VideoLinkageList.ets // 視圖層-主頁(yè)面
|---/src/main/ets/mock
| |---NewsListData.ets // 新聞列表mock數(shù)據(jù)
|---/src/main/ets/common
| |---Constants.ets // 常量數(shù)據(jù)
寫在最后
- 如果你覺得這篇內(nèi)容對(duì)你還蠻有幫助,我想邀請(qǐng)你幫我三個(gè)小忙:
- 點(diǎn)贊,轉(zhuǎn)發(fā),有你們的 『點(diǎn)贊和評(píng)論』,才是我創(chuàng)造的動(dòng)力。
- 關(guān)注小編,同時(shí)可以期待后續(xù)文章ing??,不定期分享原創(chuàng)知識(shí)。
- 想要獲取更多完整鴻蒙最新學(xué)習(xí)知識(shí)點(diǎn),請(qǐng)移步前往小編:
https://gitee.com/MNxiaona/733GH/blob/master/jianshu
