鸿蒙上实现“世界杯”主界面

最近在看社区提供的 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、赛事、积分榜。

草图如下:

效果如下:

开发环境

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

系统: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

如果您能看到最后,还希望您能动动手指点个赞,一个人能走多远关键在于与谁同行,我用跨越山海的一路相伴,希望得到您的点赞。

展开阅读全文

页面更新:2024-05-18

标签:界面   鸿蒙   球员   嵌套   世界杯   宽度   容器   组件   加载   页面   列表   图片

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top