最近在看社区提供的 app_samples,其中有一个线性容器 ArrayList,看我后让我想起 Android 中 Scroll 与 ListView 嵌套使用时需要解决的滑动冲突问题。
我想在 OpenHarmony 系统上是否也存在类似问题,Scroll 与 List 嵌套后是否存在滑动问题?
Scroll 内嵌套 List 先说个结论:
基础信息
Scroll 和 List 都属于基础容器:
Scroll:可滚动的容器组件,当子组件的布局尺寸超过父组件的尺寸时,内容可以滚动。
官方介绍:
https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/arkui-ts/ts-container-scroll.md
List:列表包含一系列相同宽度的列表项。适合连续、多行呈现同类数据,例如图片和文本。
官方介绍:
https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/arkui-ts/ts-container-list.md
需求
既然在 OpenHarmony 系统中 Scroll 与 List 不存在冲突问题,我们做一些其他的尝试,让 Scroll 与 List 的滚动结合实现联动。
场景:实现世界杯主界面,包括球员 banner、赛事、积分榜。
开发环境
实践
声明:示例中的数据的自己构建的,只为示例显示使用,与实际比赛数据存在差异,请忽略。
说明:在 DevEco Studio IDE 中构建 OpenHarmony Stage 模型项目,SDK 选择 9(3.2.7.6)。
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代表滚动到List顶部,1代表中间值,2代表滚动到List底部
const SCROLL_LIST_POSITION = {
START: 0,
CENTER: 1,
END: 2
}
const LIST_START = {
TOP: 0,
BUTTON: 1
}
class MatchDataSource extends BaseDataSource {
constructor(infos: Information[]) {
super(infos)
}
}
class BannerDataSource extends BaseDataSource {
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的随机整数
var homeIndex: number = Math.floor(Math.random() * 12) // 获取0~12的随机整数
item.homeName = MatchInfo[homeIndex].name
item.homeFlag = MatchInfo[homeIndex].resource
var awayFieldIndex: number = Math.floor(Math.random() * 12) // 获取0~12的随机整数
if (awayFieldIndex === homeIndex) {
awayFieldIndex = Math.floor(Math.random() * 12) // 获取0~12的随机整数
}
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的随机整数
var time: number = Math.floor(Math.random() * 24) // 获取0~24的随机整数
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() // 可滚动容器组件的控制器
private scrollerForList: Scroller = new Scroller()
// mock数据
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) // 滑动效果
.onReachStart(() => {
// 滑动开始
this.listPosition = SCROLL_LIST_POSITION.START
})
.onReachEnd(() => {
// 滑动结束
this.listPosition = SCROLL_LIST_POSITION.END
})
.onScrollBegin((dx: number, dy: number) => {
console.info(TAG, `listPositinotallow=${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('继续上滑 积分排名')
.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 = '进行中'
break;
}
case MatchState.NOTSTART: {
stateVal = '未开赛'
break;
}
case MatchState.CLOSED: {
stateVal = '已结束'
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)
}
}
根据代码说明下实现方式:
①3s 进入主页面,主要通过定时器 setTimeout() 实现,设置 3s 后隐藏全屏图片。
全屏图片父容器使用堆叠容器 Stack 包裹,通过 this.isShowFlashscreen 变量判断是否隐藏全屏图片,显示主页面。
②主页面中,最外层通过 Scroll 容器,作为主页面的根容器。
③球员 banner 使用滑块视图容器 Swiper,内部使用 LazyForEach 懒加载方式加载球员图片,单屏横向显示三个球员,所以球员的图片高度为屏幕总宽度的 33.3%。
并将滑块组件的 displayMode 属性设置为 SwiperDisplayMode.AutoLinear,让 Swiper 滑动一页的宽度为子组件宽度中的最大值,这样每次滑动的宽度就是 33.3%,一个球员的图片。
④赛程列表,使用 List 组件进行加载,赛事 item 使用 LazyForEach 懒加载的方式提交列表加载效率。
通过 List 中的事件监听器 onReachStart(event: () => void) 和 onReachEnd(event: () => void) 监听列表达到起始位置或底末尾位置。
并在 onScrollBegin(event: (dx: number, dy: number) => { dxRemain: number, dyRemain: number }) 函数中监听列表的滑动量,如果滑动到 List 底部,再向上滑动界面时触发显示“积分排行”界面。
⑤积分排行界面内容,初始化时超屏显示,只有在滑动到 List 底部是,才被拉起显示。
积分排行界面设置在 Scroll 容器中,通过 this.scrollerForScroll.scrollEdge(Edge.Bottom) 拉起页面。
⑥点击"返回首页",通过设置 this.scrollerForScroll.scrollEdge(Edge.Start),返回到 Scroll 顶部。
代码中使用到的组件关键 API
https://gitee.com/xjszzz9/open-harmony-ark-ui-scroll-list-o
如果您能看到最后,还希望您能动动手指点个赞,一个人能走多远关键在于与谁同行,我用跨越山海的一路相伴,希望得到您的点赞。
页面更新:2024-05-18
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号