鸿蒙上实现世界杯主界面
最近在看社区提供的 app_samples,其中有一个线性容器 ArrayList,看我后让我想起 Android 中 Scroll 与 ListView 嵌套使用时需要解决的滑动冲突问题。
我想在 OpenHarmony 系统上是否也存在类似问题,Scroll 与 List 嵌套后是否存在滑动问题?
Scroll 内嵌套 List 先说个结论: 不会出现 List 中只显示一个 item 问题 滑动事件不会冲突,在 List 区域可以滑动列表,在非 List 区域可以滑动 Scroll 滚动时,若 List 不设置宽高,则默认全部加载,在对性能有要求的场景下建议指定 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、赛事、积分榜。 启动页,3s 后进入主页面 头部显示球员 banner,首次显示 3 个球员,每隔 3 秒切换一个球员 球赛列表,包括:对战球队、比分、比赛状态(未开赛、已结束、进行中)、赛程 球赛列表拉到最后一条,触发全屏显示积分榜 点击返回首页,返回到页面顶部,球赛列表返回首条显示 在一个页面中实现 草图如下:
效果如下:
开发环境 IDE:DevEco Studio 3.0 Beta4 Build Version: 3.0.0.992, built on July 14, 2022SDK:Full SDK 9 3.2.7.6系统:OpenHarmony v3.2 beta3
实践
声明:示例中的数据的自己构建的,只为示例显示使用,与实际比赛数据存在差异,请忽略。 ①创建项目
说明:在 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①Scroll
说明:若通过 onScrollBegin 事件和 scrollBy 方法实现容器嵌套滚动,需设置子滚动节点的 EdgeEffect 为 None。如 Scroll 嵌套 List 滚动时,List 组件的 edgeEffect 属性需设置为 EdgeEffect.None。②Swiper
③List
完整代码:https://gitee.com/xjszzz9/open-harmony-ark-ui-scroll-list-o
如果您能看到最后,还希望您能动动手指点个赞,一个人能走多远关键在于与谁同行,我用跨越山海的一路相伴,希望得到您的点赞。
越南房地产暴雷,越南经济怎么了?近期全球的金融形势,主要焦点都集中在英国欧洲还有日本身上。由于美联储激进加息,叠加俄乌冲突,让欧洲反而成为最容易被收割的地区。包括之前特拉斯的大规模减税计划成为导火索,导致英债市场
2023年开门红陆续揭幕,储蓄型产品受热捧,有望带动保费增长进入四季度,一年一度的开门红号角吹响,陆续有保险公司的开门红产品亮相。尽管业内偶有淡化开门红的声音,但是,部分头部保险公司已开始摩拳擦掌。国寿寿险一如往常,率先启动开门红,推出鑫享
千禾味业零添加利好下,营收突破预期?前段时间轰轰烈烈的添加剂风波,使得海天酱油深陷舆论,虽然其一直喊着产品符合食品安全规范,但随着近年来消费升级,人们的健康意识日益增强。所以尽管海天深觉冤枉,但消费者并不买账。在资本
中国真正的4大财团有多强?各个资产超上万亿,且都属于央企前言但仍有31。7万亿美元,净利润较同期下降了20,总计为1。6万亿美元,这也创下了自2019年以来的最大跌幅。可虽然说疫情影响了这些企业的收入和利润,但对于某些实力强劲的大财团来
居民存款创新高破100万亿,富人为什么不买房,在等待什么呢?居民存款创新高破100万亿,富人为什么不买房,在等待什么呢?最近有某经济学家催人买房,为什么这么好心呢,难道是怕普通人未来买不起房吗?如果从屁股决定脑袋的角度分析,这不是什么为了购
房企化债进行时,部分债务延期5年以上以时间换空间的化债之路,是投资人与房企的唯一选择吗?作者蒲肃来源债市观察从2021年开始,房地产行业开始出现集中性债务违约,而进入2022年,则成为房企集中化债的元年,从今年开始,
格力海尔们入局预制菜市场,家电巨头们看中的是什么预制菜这一新风口,正吸引越来越多的家电巨头加入。有观点认为,预制菜符合当下快节奏的生活需求,也符合人们从吃饱到吃好吃健康吃营养的现实需求,因而具有巨大的市场潜力。多家预测数据显示,
现在的你多少岁,有多少存款?97年。存款198万8千多一点。还有三个月应该200万了吧。还有一些资产配置和借出去10万左右,利息10个点。不过收回来有困难,无所谓。钱都是家里给的遗产。孤儿,部队两年赚了15万
主力资金连续4日净买入名单曝光更新日期10月22日若不喜欢看表格,输入任何数字进入底部图片区域。连续抢筹主力资金连续多日净买入名单序号代码名称主力连买天数主力净流入(万元)最新价涨跌幅换手率量比当日通吃率当日主
大中华区继续大跌,阿迪达斯三季度净利润下跌超六成记者覃思悦北京时间2022年10月21日,德国运动品牌巨头阿迪达斯公布了2022年第三季度的初步业绩。业绩报告显示,品牌销售额同比增长11至64。08亿欧元(约合457亿元人民币)
城乡居民基础养老金最低标准的提高,这些地区走得快,步子大今年是全国城乡居民养老保险基础养老金调整的年份,前不久国国人力资源社会保障部,已下发通知,全国城乡居民养老保险基础养老金最低标准由原来的每人每月93元,提高到每人每月98元,增加了