
??想了解更多關(guān)于開源的內(nèi)容,請?jiān)L問:??
??51CTO 開源基礎(chǔ)軟件社區(qū)??
??https://ost.51cto.com??
最近在看社區(qū)提供的app_samples?,其中有一個(gè)線性容器 ArrayList,看我后讓我想起Android中Scroll與ListView嵌套使用時(shí)需要解決的滑動(dòng)沖突問題。
我想在OpenHarmony系統(tǒng)上是否也存在類似問題,Scroll與List嵌套后是否存在滑動(dòng)問題?
Scroll內(nèi)嵌套List先說個(gè)結(jié)論:
1、不會(huì)出現(xiàn)List中只顯示一個(gè)item問題;
2、滑動(dòng)事件不會(huì)沖突,在List區(qū)域可以滑動(dòng)列表,在非List區(qū)域可以滑動(dòng)Scroll;
3、滾動(dòng)時(shí),若List不設(shè)置寬高,則默認(rèn)全部加載,在對性能有要求的場景下建議指定List的寬高。
基礎(chǔ)信息
Scroll和List都屬于基礎(chǔ)容器。
Scroll:可滾動(dòng)的容器組件,當(dāng)子組件的布局尺寸超過父組件的尺寸時(shí),內(nèi)容可以滾動(dòng)。??官方介紹??List:列表包含一系列相同寬度的列表項(xiàng)。適合連續(xù)、多行呈現(xiàn)同類數(shù)據(jù),例如圖片和文本。官方介紹
需求
既然在OpenHarmony系統(tǒng)中Scroll與List不存在沖突問題,我們做一些其他的嘗試,讓Scroll與List的滾動(dòng)結(jié)合實(shí)現(xiàn)聯(lián)動(dòng)。
場景:實(shí)現(xiàn)世界杯主界面,包括球員banner、賽事、積分榜。
1、啟動(dòng)頁,3s后進(jìn)入主頁面。
2、頭部顯示球員banner,首次顯示3個(gè)球員,每隔3秒切換一個(gè)球員。
3、球賽列表,包括:對戰(zhàn)球隊(duì)、比分、比賽狀態(tài)(未開賽、已結(jié)束、進(jìn)行中)、賽程。
4、球賽列表拉到最后一條,觸發(fā)全屏顯示積分榜。
5、點(diǎn)擊返回首頁,返回到頁面頂部,球賽列表返回首條顯示。
6、在一個(gè)頁面中實(shí)現(xiàn)。
草圖

效果
??演示視頻地址??




開發(fā)環(huán)境
- IDE:DevEco Studio 3.0 Beta4 Build Version: 3.0.0.992, built on July 14, 2022
- SDK:Full SDK 9 3.2.7.6
- 系統(tǒng):OpenHarmony v3.2 beta3
實(shí)踐
聲明:示例中的數(shù)據(jù)的自己構(gòu)建的,只為示例顯示使用,與實(shí)際比賽數(shù)據(jù)存在差異,請忽略。
1、創(chuàng)建項(xiàng)目
說明:在DevEco Studio IDE中構(gòu)建OpenHarmony Stage模型項(xiàng)目,SDK選擇9(3.2.7.6)

2、關(guān)鍵代碼
import { BaseDataSource } from '../MainAbility/model/BaseDataSource'
import { Information } from '../MainAbility/model/Information'
import { MatchInfo, MatchState } from '../MainAbility/common/FlagData'
import { MatchDataResource } from '../MainAbility/model/MatchDataResource'
import { BannerDataResource } from '../MainAbility/model/BannerDataResource'
const TAG: string = 'ScrollList'
// 0代表滾動(dòng)到List頂部,1代表中間值,2代表滾動(dòng)到List底部
const SCROLL_LIST_POSITION = {
START: 0,
CENTER: 1,
END: 2
}
const LIST_START = {
TOP: 0,
BUTTON: 1
}
class MatchDataSource extends BaseDataSource<Information> {
constructor(infos: Information[]) {
super(infos)
}
}
class BannerDataSource extends BaseDataSource<BannerDataResource> {
constructor(infos: BannerDataResource[]) {
super(infos)
}
}
function mock(): Information[] {
var infos = []
for (var i = 0; i < 10; i++) {
var item = new Information()
item.id = i
item.state = Math.floor(Math.random() * 2) // 獲取0~2的隨機(jī)整數(shù)
var homeIndex: number = Math.floor(Math.random() * 12) // 獲取0~12的隨機(jī)整數(shù)
item.homeName = MatchInfo[homeIndex].name
item.homeFlag = MatchInfo[homeIndex].resource
var awayFieldIndex: number = Math.floor(Math.random() * 12) // 獲取0~12的隨機(jī)整數(shù)
if (awayFieldIndex === homeIndex) {
awayFieldIndex = Math.floor(Math.random() * 12) // 獲取0~12的隨機(jī)整數(shù)
}
item.awayFieldName = MatchInfo[awayFieldIndex].name
item.awayFieldFlag = MatchInfo[awayFieldIndex].resource
if (item.state != MatchState.NOTSTART) {
item.homeScore = Math.floor(Math.random() * 6)
item.awayFiledScore = Math.floor(Math.random() * 6)
}
var data: number = Math.floor(Math.random() * 20) // 獲取0~20的隨機(jī)整數(shù)
var time: number = Math.floor(Math.random() * 24) // 獲取0~24的隨機(jī)整數(shù)
item.gameTime = '12 - ' + data + ' ' + time + ' : 00'
infos[i] = item
}
return infos
}
function mockBanner(): BannerDataResource[] {
var banners = [{
id: 1,
resource: $r('app.media.banner_01')
},
{
id: 2,
resource: $r('app.media.banner_02')
},
{
id: 3,
resource: $r('app.media.banner_03')
},
{
id: 4,
resource: $r('app.media.banner_04')
},
{
id: 5,
resource: $r('app.media.banner_05')
}
]
return banners
}
@Entry
@Component
struct Index {
private listPosition: number = SCROLL_LIST_POSITION.START
@State private listState: number = LIST_START.TOP
private scrollerForScroll: Scroller = new Scroller() // 可滾動(dòng)容器組件的控制器
private scrollerForList: Scroller = new Scroller()
// mock數(shù)據(jù)
private matchData: Information[] = mock()
private matchDataSource: MatchDataSource = new MatchDataSource(this.matchData)
// banner
private bannerData: BannerDataResource[] = mockBanner()
private bannerDataSource: BannerDataSource = new BannerDataSource(this.bannerData)
private swiperController: SwiperController = new SwiperController()
@State private isShowFlashscreen: boolean = true
private timeOutID: number
aboutToAppear() {
this.startTimeout()
}
aboutToDisappear() {
this.stopTimeout()
}
build() {
Stack() {
if (this.isShowFlashscreen) {
Image($r('app.media.flashscreen'))
.width('100%')
.height('100%')
.objectFit(ImageFit.Cover)
} else {
Scroll(this.scrollerForScroll) {
Column() {
Swiper(this.swiperController) {
LazyForEach(this.bannerDataSource, (item: BannerDataResource) => {
Image(item.resource)
.width('33.3%')
.height('100%')
.objectFit(ImageFit.Cover)
}, item => item.id.toString())
}
.width('100%')
.height('35%')
.cachedCount(3)
.index(0)
.autoPlay(true)
.loop(true)
.displayMode(SwiperDisplayMode.AutoLinear)
.indicator(false)
.indicatorStyle({
selectedColor: $r('app.color.red_bg')
})
Divider().strokeWidth(3).color($r('app.color.red_bg'))
Column() {
List({
space: 10,
scroller: this.scrollerForList
}) {
LazyForEach(this.matchDataSource, (item: Information) => {
ListItem() {
Row() {
Column({ space: 10 }) {
Image(item.homeFlag)
.width(60)
.height(45)
.objectFit(ImageFit.Contain)
Text(item.homeName)
.width('100%')
.fontSize(16)
.textAlign(TextAlign.Center)
}
.width('30%')
Column({ space: 10 }) {
Text(this.getMatchState(item.state))
.width('100%')
.fontSize(12)
.fontColor($r('app.color.event_text'))
.textAlign(TextAlign.Center)
Text(this.getMatchSource(item))
.width('100%')
.fontSize(18)
.textAlign(TextAlign.Center)
Text(item.gameType)
.width('100%')
.fontSize(12)
.fontColor($r('app.color.event_text'))
.textAlign(TextAlign.Center)
}
.width('30%')
Column({ space: 10 }) {
Image(item.awayFieldFlag)
.width(60)
.height(45)
.objectFit(ImageFit.Contain)
Text(item.awayFieldName)
.width('100%')
.fontSize(16)
.textAlign(TextAlign.Center)
}
.width('30%')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.SpaceBetween)
.border({
radius: 15
})
.backgroundColor($r('app.color.white'))
}
.width('100%')
.height(95)
}, item => item.id.toString())
}
.width('90%')
.height('100%')
.edgeEffect(EdgeEffect.Spring) // 滑動(dòng)效果
.onReachStart(() => {
// 滑動(dòng)開始
this.listPosition = SCROLL_LIST_POSITION.START
})
.onReachEnd(() => {
// 滑動(dòng)結(jié)束
this.listPosition = SCROLL_LIST_POSITION.END
})
.onScrollBegin((dx: number, dy: number) => {
console.info(TAG, `listPosition=${this.listPosition} dx=${dx} ,dy=${dy}`)
if (this.listPosition == SCROLL_LIST_POSITION.START && dy >= 0) {
// 列表頂部
// this.scrollerForScroll.scrollBy(0, -dy)
this.scrollerForScroll.scrollEdge(Edge.Start)
this.listState = LIST_START.TOP
} else if (this.listPosition == SCROLL_LIST_POSITION.END && dy <= 0) {
// 列表底部
// this.scrollerForScroll.scrollBy(0, -dy)
this.scrollerForScroll.scrollEdge(Edge.Bottom)
this.listState = LIST_START.BUTTON
}
this.listPosition = SCROLL_LIST_POSITION.CENTER
return {
dxRemain: dx,
dyRemain: dy
}
})
}
.width('100%')
.height('60%')
.padding({
top: 20,
bottom: 20
})
.borderRadius({
bottomLeft: 15,
bottomRight: 15
})
.backgroundColor($r('app.color.content_bg'))
Column() {
if (this.listState === LIST_START.TOP) {
Text('繼續(xù)上滑 積分排名')
.width('100%')
.height('5%')
.fontColor($r('app.color.white'))
.fontSize(14)
.textAlign(TextAlign.Center)
} else {
Text('回到首頁')
.width('100%')
.height('5%')
.fontColor($r('app.color.white'))
.fontSize(14)
.textAlign(TextAlign.Center)
.onClick(() => {
this.scrollerForScroll.scrollEdge(Edge.Start)
this.scrollerForList.scrollToIndex(0)
this.listState = LIST_START.TOP
})
}
Stack() {
Image($r('app.media.result_1'))
.width('100%')
.height('100%')
.objectFit(ImageFit.Cover)
Column() {
}.width('100%')
.height('100%')
.backgroundColor('#55000000')
Image($r('app.media.football_poster'))
.width('100%')
.height('100%')
.objectFit(ImageFit.Contain)
.opacity(0.70)
.borderRadius({
topLeft: 15,
topRight: 15
})
}.width('100%')
.height('95%')
}
.width('100%')
.height('100%')
}
}
.width('100%')
.height('100%')
.onScrollBegin((dx: number, dy: number) => {
return {
dxRemain: dx,
dyRemain: 0
}
})
}
}.width('100%')
.height('100%')
.backgroundColor($r('app.color.main_bg'))
}
getMatchState(state: number): string {
var stateVal: string
switch (state) {
case MatchState.PROGRESS: {
stateVal = '進(jìn)行中'
break;
}
case MatchState.NOTSTART: {
stateVal = '未開賽'
break;
}
case MatchState.CLOSED: {
stateVal = '已結(jié)束'
break;
}
default:
stateVal = ''
}
return stateVal;
}
getMatchSource(data: Information): string {
if (data.state === MatchState.NOTSTART) {
return '- : -'
} else {
return data.homeScore + ' : ' + data.awayFiledScore
}
}
startTimeout() {
this.timeOutID = setTimeout(() => {
this.isShowFlashscreen = false
}, 3000)
}
stopTimeout() {
clearTimeout(this.timeOutID)
}
}
根據(jù)代碼說明下實(shí)現(xiàn)方式
1、3s進(jìn)入主頁面,主要通過定時(shí)器setTimeout()實(shí)現(xiàn),設(shè)置3s后隱藏全屏圖片,全屏圖片父容器使用堆疊容器Stack包裹,通過this.isShowFlashscreen變量判斷是否隱藏全屏圖片,顯示主頁面。
2、主頁面中,最外層通過Scroll容器,作為主頁面的根容器。
3、球員banner使用滑塊視圖容器Swiper,內(nèi)部使用LazyForEach 懶加載方式加載球員圖片,單屏橫向顯示三個(gè)球員,所以球員的圖片高度為屏幕總寬度的33.3%,并將滑塊組件的displayMode屬性設(shè)置為SwiperDisplayMode.AutoLinear,讓Swiper滑動(dòng)一頁的寬度為子組件寬度中的最大值,這樣每次滑動(dòng)的寬度就是33.3%,一個(gè)球員的圖片。
4、賽程列表,使用List組件進(jìn)行加載,賽事item使用LazyForEach懶加載的方式提交列表加載效率,通過List中的事件監(jiān)聽器onReachStart(event: () => void)和onReachEnd(event: () => void) 監(jiān)聽列表達(dá)到起始位置或底末尾位置,并在onScrollBegin(event: (dx: number, dy: number) => { dxRemain: number, dyRemain: number })函數(shù)中監(jiān)聽列表的滑動(dòng)量,如果滑動(dòng)到List底部,再向上滑動(dòng)界面時(shí)觸發(fā)顯示“積分排行”界面。
5、積分排行界面內(nèi)容,初始化時(shí)超屏顯示,只有在滑動(dòng)到List底部是,才被拉起顯示,積分排行界面設(shè)置在Scroll容器中,通過this.scrollerForScroll.scrollEdge(Edge.Bottom) 拉起頁面。
6、點(diǎn)擊"返回首頁",通過設(shè)置this.scrollerForScroll.scrollEdge(Edge.Start),返回到Scroll頂部。
代碼中使用到的組件關(guān)鍵API
Scroll
名稱 | 功能描述 |
onScrollBegin<sup>9+</sup>(event: (dx: number, dy: number) => { dxRemain: number, dyRemain: number }) | 滾動(dòng)開始事件回調(diào)。 參數(shù): - dx:即將發(fā)生的水平方向滾動(dòng)量。 - dy:即將發(fā)生的豎直方向滾動(dòng)量。 返回值: - dxRemain:水平方向滾動(dòng)剩余量。 - dyRemain:豎直方向滾動(dòng)剩余量。 |
說明:
若通過onScrollBegin事件和scrollBy方法實(shí)現(xiàn)容器嵌套滾動(dòng),需設(shè)置子滾動(dòng)節(jié)點(diǎn)的EdgeEffect為None。如Scroll嵌套List滾動(dòng)時(shí),List組件的edgeEffect屬性需設(shè)置為EdgeEffect.None。
Swiper
名稱 | 參數(shù)類型 | 描述 |
index | number | 設(shè)置當(dāng)前在容器中顯示的子組件的索引值。<br/>默認(rèn)值:0 |
autoPlay | boolean | 子組件是否自動(dòng)播放,自動(dòng)播放狀態(tài)下,導(dǎo)航點(diǎn)不可操作。<br/>默認(rèn)值:false |
interval | number | 使用自動(dòng)播放時(shí)播放的時(shí)間間隔,單位為毫秒。<br/>默認(rèn)值:3000 |
indicator | boolean | 是否啟用導(dǎo)航點(diǎn)指示器。<br/>默認(rèn)值:true |
loop | boolean | 是否開啟循環(huán)。 設(shè)置為true時(shí)表示開啟循環(huán),在LazyForEach懶循環(huán)加載模式下,加載的組件數(shù)量建議大于5個(gè)。<br/>默認(rèn)值:true |
duration | number | 子組件切換的動(dòng)畫時(shí)長,單位為毫秒。<br/>默認(rèn)值:400 |
vertical | boolean | 是否為縱向滑動(dòng)。<br/>默認(rèn)值:false |
itemSpace | number | string | 設(shè)置子組件與子組件之間間隙。<br/>默認(rèn)值:0 |
displayMode | SwiperDisplayMode | 主軸方向上元素排列的模式,優(yōu)先以displayCount設(shè)置的個(gè)數(shù)顯示,displayCount未設(shè)置時(shí)本屬性生效。<br/>默認(rèn)值:SwiperDisplayMode.Stretch |
cachedCount<sup>8+</sup> | number | 設(shè)置預(yù)加載子組件個(gè)數(shù)。<br/>默認(rèn)值:1 |
disableSwipe<sup>8+</sup> | boolean | 禁用組件滑動(dòng)切換功能。<br/>默認(rèn)值:false |
curve<sup>8+</sup> | Curve | string | 設(shè)置Swiper的動(dòng)畫曲線,默認(rèn)為淡入淡出曲線,常用曲線參考Curve枚舉說明,也可以通過[]插值計(jì)算模塊提供的接口創(chuàng)建自定義的插值曲線對象。<br/>默認(rèn)值:Curve.Ease |
indicatorStyle<sup>8+</sup> | {<br/>left?: Length,<br/>top?: Length,<br/>right?: Length,<br/>bottom?: Length,<br/>size?: Length,<br/>mask?: boolean,<br/>color?: ResourceColor,<br/>selectedColor?: ResourceColor<br/>} | 設(shè)置導(dǎo)航點(diǎn)樣式:<br/>- left: 設(shè)置導(dǎo)航點(diǎn)距離Swiper組件左邊的距離。<br/>- top: 設(shè)置導(dǎo)航點(diǎn)距離Swiper組件頂部的距離。<br/>- right: 設(shè)置導(dǎo)航點(diǎn)距離Swiper組件右邊的距離。<br/>- bottom: 設(shè)置導(dǎo)航點(diǎn)距離Swiper組件底部的距離。<br/>- size: 設(shè)置導(dǎo)航點(diǎn)的直徑。<br/>- mask: 設(shè)置是否顯示導(dǎo)航點(diǎn)蒙層樣式。<br/>- color: 設(shè)置導(dǎo)航點(diǎn)的顏色。<br/>- selectedColor: 設(shè)置選中的導(dǎo)航點(diǎn)的顏色。 |
displayCount<sup>8+</sup> | number|string | 設(shè)置一頁內(nèi)元素顯示個(gè)數(shù)。<br/>默認(rèn)值:1 |
effectMode<sup>8+</sup> | EdgeEffect | 滑動(dòng)效果,目前支持的滑動(dòng)效果參見EdgeEffect的枚舉說明。<br/>默認(rèn)值:EdgeEffect.Spring |
List
名稱 | 功能描述 |
onReachStart(event: () => void) | 列表到達(dá)起始位置時(shí)觸發(fā)。 |
onReachEnd(event: () => void) | 列表到底末尾位置時(shí)觸發(fā)。 |
onScrollBegin<sup>9+</sup>(event: (dx: number, dy: number) => { dxRemain: number, dyRemain: number }) | 列表開始滑動(dòng)時(shí)觸發(fā),事件參數(shù)傳入即將發(fā)生的滑動(dòng)量,事件處理函數(shù)中可根據(jù)應(yīng)用場景計(jì)算實(shí)際需要的滑動(dòng)量并作為事件處理函數(shù)的返回值返回,列表將按照返回值的實(shí)際滑動(dòng)量進(jìn)行滑動(dòng)。<br/>- dx:即將發(fā)生的水平方向滑動(dòng)量。<br/>- dy:即將發(fā)生的豎直方向滑動(dòng)量。<br/>- dxRemain:水平方向?qū)嶋H滑動(dòng)量。<br/>- dyRemain:豎直方向?qū)嶋H滑動(dòng)量。 |
??想了解更多關(guān)于開源的內(nèi)容,請?jiān)L問:??
??51CTO 開源基礎(chǔ)軟件社區(qū)??
??https://ost.51cto.com??