
??想了解更多關(guān)于開源的內(nèi)容,請(qǐng)?jiān)L問:??
??51CTO 開源基礎(chǔ)軟件社區(qū)??
??https://ost.51cto.com??
效果
??在線視頻??
接??上一篇??,閃屏頁(yè)面跳轉(zhuǎn)到主頁(yè),接下來(lái)我們?cè)敿?xì)的說(shuō)說(shuō)主頁(yè)開發(fā)涉及的內(nèi)容,首先我們來(lái)看下主頁(yè)是設(shè)計(jì)圖,如下:

簡(jiǎn)單來(lái)說(shuō)頁(yè)面分成上下兩部分,上半部分是一個(gè)橫向滾動(dòng)的banner,下半部分是電影資源的列表,列表中的一行兩列均分,每一個(gè)資源信息包括:電影資源的宣傳圖、電影名稱、演員、電影亮點(diǎn)。
項(xiàng)目開發(fā)
開發(fā)環(huán)境
硬件平臺(tái):DAYU2000 RK3568
系統(tǒng)版本:OpenHarmony 3.2 beta5
SDK:9(3.2.10.6)
IDE:DevEco Studio 3.1 Beta1 Build Version: 3.1.0.200, built on February 13, 2023
程序代碼
Index.ets
import { VideoDataSource } from '../model/VideoDataSource'
import { VideoData } from '../model/VideoData'
import { MockVideoData } from '../model/MockVideoData'
import router from '@ohos.router';
import { VideoListView } from '../view/VideoListView'
const TAG: string = 'Splash Index'
@Entry
@Component
struct Index {
  @State bannerList: Array<VideoData> = []
  @State videoList: Array<VideoData> = []
  private scrollerForScroll: Scroller = new Scroller()
  @State @Watch('scrollChange') scrollIndex: number = 0
  @State opacity1: number = 0
  aboutToAppear() {
    this.initData()
    router.clear()
  }
  scrollChange() {
    if (this.scrollIndex === 0) {
      this.scrollToAnimation(0, 0)
    } else if (this.scrollIndex === 2) {
      this.scrollToAnimation(0, 300)
    }
  }
  scrollToAnimation(xOffset, yOffset) {
    this.scrollerForScroll.scrollTo({
      xOffset: xOffset,
      yOffset: yOffset,
      animation: {
        duration: 3000,
        curve: Curve.FastOutSlowIn
      }
    })
  }
  initData() {
    this.bannerList = MockVideoData.getBannerList()
    this.videoList = MockVideoData.getVideoList()
  }
  build() {
    Column() {
      Scroll(this.scrollerForScroll) {
        Column() {
          // banner
          Swiper() {
            LazyForEach(new VideoDataSource(this.bannerList), (item: VideoData) => {
              Image(item.image)
                .width('100%')
                .height('100%')
                .border({
                  radius: 20
                })
                .onClick(() => {
                  router.pushUrl({ url: 'pages/Playback',
                    params: {
                      video_data: item
                    } })
                })
                .objectFit(ImageFit.Fill)
            }, item => item.id)
          }
          .width('100%')
          .height(240)
          .itemSpace(20)
          .autoPlay(true)
          .indicator(false)
          .cachedCount(3)
          .margin({
            bottom: 20
          })
          VideoListView({
            videoList: $videoList,
            scrollIndex: $scrollIndex,
            isBlackModule: false
          })
        }.width('100%')
      }
      .scrollBar(BarState.Off)
      .scrollable(ScrollDirection.Vertical)
      .scrollBarColor(Color.Gray)
      .scrollBarWidth(30)
      .edgeEffect(EdgeEffect.Spring)
    }
    .width('100%')
    .height('100%')
    .backgroundImage($r('app.media.main_bg'), ImageRepeat.XY)
    .padding(20)
  }
  pageTransition() {
    PageTransitionEnter({ duration: 1500,
      type: RouteType.Push,
      curve: Curve.Linear })
      .opacity(this.opacity1)
      .onEnter((type: RouteType, progress: number) => {
        console.info(`${TAG} PageTransitionEnter onEnter type:${type} progress:${progress}`)
        this.opacity1 = progress
      })
  }
}開發(fā)詳解
1、電影數(shù)據(jù)
界面內(nèi)容需要通過(guò)數(shù)據(jù)進(jìn)行加載,目前沒有相關(guān)的電影云端,我們就先在本地mock出一些電影數(shù)據(jù),每個(gè)電影數(shù)據(jù)都應(yīng)該包含以下屬性,我們定義了一個(gè)類來(lái)表示:
VideoData.ets
export class VideoData {
  id: string
  name: string // 名稱
  describe: string // 描述
  resourceType: string // 資源類型 出品年限 類型
  source:string // 來(lái)源
  introduction: string // 介紹
  uri: string | Resource // 資源地址
  image: string | Resource // 資源圖片
  actors: User[] //參演者
  heat: number // 熱度
  directs:User[] // 導(dǎo)演
  grade:string // 評(píng)分
  gradeNumber : string // 參與評(píng)分人數(shù)
}
export class User{
  id: number
  name: string
  role: string
  icon: string | Resource
}2、構(gòu)建數(shù)據(jù)
溫馨提示:電影相關(guān)的數(shù)據(jù)是本地模擬,除了電影名稱和電影宣傳圖相關(guān),其他信息純屬虛構(gòu),如果你感興趣也可以自己構(gòu)建。
這個(gè)很簡(jiǎn)單,就是根據(jù)VideoData所定義的數(shù)據(jù),構(gòu)建出首頁(yè)需要顯示的內(nèi)容,因?yàn)閙ock的數(shù)據(jù)都是自定義的,所以這里就帖部分代碼,如果你有興趣可以自行構(gòu)造,如下所示:
MockVideoData.ets
export class MockVideoData {
  static getVideoList(): Array<VideoData> {
    let data: Array<VideoData> = new Array()
    // 電影
    data.push(this.createVideoDataByImage('鐵道英雄', $r('app.media.v1')))
    data.push(this.createVideoDataByImage('沙丘', $r('app.media.v2')))
    data.push(this.createVideoDataByImage('那一夜我給你開過(guò)車', $r('app.media.v3')))
    data.push(this.createVideoDataByImage('雷神2', $r('app.media.v4')))
    data.push(this.createVideoDataByImage('大圣歸來(lái)', $r('app.media.v5')))
    data.push(this.createVideoDataByImage('流浪地球', $r('app.media.v6')))
    data.push(this.createVideoDataByImage('狄仁杰', $r('app.media.v7')))
    data.push(this.createVideoDataByImage('獨(dú)行月球', $r('app.media.v8')))
    data.push(this.createVideoDataByImage('消失的子彈', $r('app.media.v9')))
    data.push(this.createVideoDataByImage('西游降魔篇', $r('app.media.v10')))
    data.push(this.createVideoDataByImage('激戰(zhàn)', $r('app.media.v11')))
    data.push(this.createVideoDataByImage('作妖轉(zhuǎn)', $r('app.media.v12')))
    data.push(this.createVideoDataByImage('滅絕', $r('app.media.v13')))
    data.push(this.createVideoDataByImage('獨(dú)行月球', $r('app.media.v14')))
    data.push(this.createVideoDataByImage('超人·素人特工', $r('app.media.v15')))
    data.push(this.createVideoDataByImage('戰(zhàn)狼2', $r('app.media.v16')))
    data.push(this.createVideoDataByImage('四大名捕', $r('app.media.v17')))
    data.push(this.createVideoDataByImage('無(wú)人區(qū)', $r('app.media.v18')))
    data.push(this.createVideoDataByImage('邪不壓正', $r('app.media.v19')))
    return data
  }
  private static createVideoDataByImage(_name, _image, uri?): VideoData {
    if (typeof (uri) === 'undefined') {
      uri = $rawfile('video_4.mp4')
    }
    return this.createVideoData(
      _name,
      '硬漢強(qiáng)力回歸',
      '2023 / 動(dòng)作 / 槍戰(zhàn)',
      '愛電影',
      '《邪不壓正》是由姜文編劇并執(zhí)導(dǎo),姜文、彭于晏、廖凡、周韻、許晴、澤田謙也等主演的動(dòng)作喜劇電影。該片改編自張北海小說(shuō)《俠隱》。講述在1937年\“七七事變\”爆發(fā)之前,北平城的“至暗時(shí)刻”,一個(gè)身負(fù)大恨、自美歸國(guó)的特工李天然,在國(guó)難之時(shí)滌蕩重重陰謀上演的一出終極復(fù)仇記。',
      uri,
      _image
    )
  }
  private static createVideoData(_name, _describe, _resourceType, _source, _introduction, _uri, _image,): VideoData {
    let vData: VideoData = new VideoData()
    vData.id = UUIDUtils.getUUID()
    vData.name = _name
    vData.describe = _describe
    vData.resourceType = _resourceType
    vData.source = _source
    vData.introduction = _introduction
    vData.uri = _uri
    vData.image = _image
    vData.actors = []
    let user1: User = new User()
    user1.name = '吳京'
    user1.role = '飾 吳曉曉'
    user1.icon = $r('app.media.actor_02')
    vData.actors.push(user1)
    let user2: User = new User()
    user2.name = '屈楚蕭'
    user2.role = '飾 吳曉曉'
    user2.icon = $r('app.media.actor_03')
    vData.actors.push(user2)
    let user3: User = new User()
    user3.name = '吳京'
    user3.role = '飾 吳曉曉'
    user3.icon = $r('app.media.actor_02')
    vData.actors.push(user3)
    vData.heat = 89
    vData.grade = '8.6'
    vData.gradeNumber = '3.6萬(wàn)'
    vData.directs = []
    for (let i = 0; i < 1; i++) {
      let user: User = new User()
      user.name = '戴維'
      user.role = '導(dǎo)演'
      user.icon = $r('app.media.actor_01')
      vData.directs.push(user)
    }
    return vData
  }
  
  
 static getBannerList(): Array<VideoData> {
    let data: Array<VideoData> = new Array()
    // 構(gòu)建banner數(shù)據(jù),與構(gòu)建videoData類似
    return data
    }
  }3、banner
在Index.ets的aboutToAppear()函數(shù)中初始化數(shù)據(jù),通過(guò)MockVideoData.getBannerList()獲取到banner列表,使用Swiper滑塊組件實(shí)現(xiàn)自動(dòng)輪播顯示。在Swiper容器中使用了LazyForEach懶加載的方式進(jìn)行子項(xiàng)的加載。簡(jiǎn)單說(shuō)明下LazyForEach懶加載機(jī)制,由于在長(zhǎng)列表渲染中會(huì)涉及到大量的數(shù)據(jù)加載,如果處理不當(dāng)會(huì)導(dǎo)致資源占用影響性能,在ArkUI3.0針對(duì)這樣的情況提供了一種懶加載機(jī)制,它會(huì)自動(dòng)根據(jù)具體的情況計(jì)算出適合渲染的數(shù)據(jù),實(shí)現(xiàn)數(shù)據(jù)的按需加載,提升UI刷新效率。
4、電影列表
在Index.ets的aboutToAppear()函數(shù)中初始化數(shù)據(jù),通過(guò)MockVideoData.getVideoList()獲取到Video列表,因?yàn)殡娪傲斜淼牟季衷陧?xiàng)目中其他模塊也會(huì)使用到,所以這里將電影列表抽象出一個(gè)子組件VideoListView。
VideoListView.ets
/**
 * 視頻列表
 */
import { VideoData } from '../model/VideoData'
import { VideoDataSource } from '../model/VideoDataSource'
import { VideoDataUtils } from '../utils/VideoDataUtils'
import router from '@ohos.router';
const TAG: string = 'VideoListView'
@Component
export struct VideoListView {
  private scrollerForGrid: Scroller = new Scroller()
  @Link videoList: Array<VideoData>
  @Link scrollIndex: number
  @Prop isBlackModule: boolean //是否為黑色模式
  build() {
    // 電影列表
    Grid(this.scrollerForGrid) {
      LazyForEach(new VideoDataSource(this.videoList), (item: VideoData) => {
        GridItem() {
          Column() {
            Image(item.image)
              .width(200)
              .height(250)
              .objectFit(ImageFit.Cover)
              .border({
                width: this.isBlackModule ? 0 : 1,
                color: '#5a66b1',
                radius: 10
              })
            Text(item.name)
              .width(200)
              .height(20)
              .fontColor(this.isBlackModule ? Color.Black : Color.White)
              .fontSize(16)
              .maxLines(1)
              .textOverflow({
                overflow: TextOverflow.Ellipsis
              })
              .margin({
                top: 10
              })
            Text(VideoDataUtils.getUser(item.actors))
              .width(200)
              .height(20)
              .fontColor(this.isBlackModule ? $r('app.color.name_black') : $r('app.color.name_grey'))
              .fontSize(12)
              .maxLines(1)
              .textOverflow({
                overflow: TextOverflow.Ellipsis
              })
            Text(item.describe)
              .width(200)
              .height(20)
              .fontColor(this.isBlackModule ? $r('app.color.describe_black') : $r('app.color.describe_grey'))
              .fontSize(12)
              .maxLines(1)
              .textOverflow({
                overflow: TextOverflow.Ellipsis
              })
          }.width('100%')
          .margin({
            bottom: 10
          })
          .onClick(() => {
            router.pushUrl({ url: 'pages/Playback',
              params: {
                video_data: item
              } }, router.RouterMode.Single)
          })
        }
      }, item => item.id)
    }
    .columnsTemplate('1fr 1fr')
    .columnsGap(10)
    .editMode(true)
    .cachedCount(6)
    .width('100%')
    .height('100%')
    .border({
      width: 0,
      color: Color.White
    })
    .onScrollIndex((first: number) => {
      console.info(`${TAG} onScrollIndex ${first}`)
      this.scrollIndex = first
      if (first === 0) {
        this.scrollerForGrid.scrollToIndex(0)
      }
    })
  }
}
使用Grid實(shí)現(xiàn)電影列表,由于電影列表屬于長(zhǎng)列表數(shù)據(jù),所以這里也使用了LazyForEach懶加載機(jī)制進(jìn)行item子項(xiàng)的加載,最終通過(guò)import的方式引入到Index.ets頁(yè)面中,并在布局中添加此組件。
5、滾動(dòng)頁(yè)面
首頁(yè)是電影列表頁(yè),需要加載banner和電影列表,所以整體頁(yè)面都需要可滾動(dòng),因此在banner和視頻列表容器外添加了Scroll組件。
問題1:Scroll與Grid列表嵌套時(shí),電影列表無(wú)法顯示完整,或者無(wú)法顯示banner。
如下所示:

為了描述清楚這個(gè)問題,我們將界面可以觸發(fā)滑動(dòng)的區(qū)域分為banner部分和VideoList部分,根據(jù)滑動(dòng)的觸發(fā)區(qū)域不同,進(jìn)行如下說(shuō)明:

1、觸發(fā)滑動(dòng)區(qū)域在VideoList,當(dāng)滑動(dòng)到VideoList末尾時(shí)會(huì)出現(xiàn)最后一列的item只顯示了部分,滑動(dòng)區(qū)域在VideoList的時(shí)候無(wú)論怎么向上滑動(dòng)都無(wú)法顯示完整;
2、在1的場(chǎng)景下,觸發(fā)滑動(dòng)區(qū)域在banner,并向上滑動(dòng),此時(shí)可以看到,頁(yè)面整體向上移動(dòng),VideoList中缺失的item部分可以正常顯示,banner劃出界面時(shí),VideoList可以顯示完整;
3、在2的場(chǎng)景下,整個(gè)界面目前都是VideoList區(qū)域,VideoList已滑動(dòng)到的最后,此時(shí)向下滑動(dòng),因?yàn)橛|發(fā)的區(qū)域是VideoList,所以整個(gè)VideoList向下滑動(dòng)顯示,直到電影列表首項(xiàng),由于整個(gè)頁(yè)面的可滑動(dòng)區(qū)域都是VideoLIst,無(wú)法在觸發(fā)Scroll的滑動(dòng),所以banner無(wú)法顯示。
這個(gè)問題其實(shí)就是界面視圖高度計(jì)算和觸發(fā)滑動(dòng)監(jiān)聽被消費(fèi)后無(wú)法再向上層傳遞導(dǎo)致,解決這個(gè)問題有多種方式,下面我介紹其中一種。
解決方案:Scroll組件中可以添加一個(gè)Scroller滑動(dòng)組件的控制器,控制器可以控制組件的滾動(dòng),比如滾動(dòng)的指定高度,或者指定的index,在Grid中也可以添加一個(gè)Scroller控制器進(jìn)行列表高度控制,在Grid還可以通過(guò)onScrollIndex()事件監(jiān)聽網(wǎng)格顯示的起始位置item發(fā)生變化,返回當(dāng)前的item坐標(biāo)。當(dāng)滑動(dòng)區(qū)域在VideoList時(shí),如果item坐標(biāo)發(fā)生了變化,就更新scrollIndex,在Index.ets中監(jiān)聽scrollIndex的變化,當(dāng)scrollIndex=0時(shí)表示已經(jīng)滑動(dòng)到VideoList首項(xiàng),此時(shí)再向下滑動(dòng)時(shí)控制Scroll的控制器,讓Scroll滑動(dòng)到(0,0)位置,也就是頁(yè)面頂部,這樣就可以顯示banner;當(dāng)scrollIndex=2時(shí),表示VideoList向上滑動(dòng)到第二列,此時(shí)設(shè)置外層Scroll容器的滑動(dòng)高度,讓banner劃出界面,使得VideoList可以完整顯示。
實(shí)現(xiàn)核心代碼
1、Index.ets
import { VideoDataSource } from '../model/VideoDataSource'
import { VideoData } from '../model/VideoData'
import { MockVideoData } from '../model/MockVideoData'
import router from '@ohos.router';
import { VideoListView } from '../view/VideoListView'
const TAG: string = 'Splash Index'
@Entry
@Component
struct Index {
  private scrollerForScroll: Scroller = new Scroller()
  @State @Watch('scrollChange') scrollIndex: number = 0
  scrollChange() {
    if (this.scrollIndex === 0) {
      this.scrollToAnimation(0, 0)
    } else if (this.scrollIndex === 2) {
      this.scrollToAnimation(0, 300)
    }
  }
  scrollToAnimation(xOffset, yOffset) {
    this.scrollerForScroll.scrollTo({
      xOffset: xOffset,
      yOffset: yOffset,
      animation: {
        duration: 3000,
        curve: Curve.FastOutSlowIn
      }
    })
  }
  build() {
    Column() {
      Scroll(this.scrollerForScroll) {
        Column() {
          // banner
          VideoListView({
            videoList: $videoList,
            scrollIndex: $scrollIndex,
            isBlackModule: false
          })
        }.width('100%')
      }
      .scrollBar(BarState.Off)
      .scrollable(ScrollDirection.Vertical)
      .scrollBarColor(Color.Gray)
      .scrollBarWidth(30)
      .edgeEffect(EdgeEffect.Spring)
    }
    .width('100%')
    .height('100%')
    .backgroundImage($r('app.media.main_bg'), ImageRepeat.XY)
    .padding(20)
  }
}2、VideoListView.ets
/**
 * 視頻列表
 */
import { VideoData } from '../model/VideoData'
import { VideoDataSource } from '../model/VideoDataSource'
import { VideoDataUtils } from '../utils/VideoDataUtils'
import router from '@ohos.router';
const TAG: string = 'VideoListView'
@Component
export struct VideoListView {
  private scrollerForGrid: Scroller = new Scroller()
  @Link scrollIndex: number
  build() {
    // 電影列表
    Grid(this.scrollerForGrid) {
      LazyForEach(new VideoDataSource(this.videoList), (item: VideoData) => {
        GridItem() {
         // item
          }.width('100%')
          .margin({
            bottom: 10
          })
          .onClick(() => {
            router.pushUrl({ url: 'pages/Playback',
              params: {
                video_data: item
              } }, router.RouterMode.Single)
          })
        }
      }, item => item.id)
    }
    .columnsTemplate('1fr 1fr')
    .columnsGap(10)
    .editMode(true)
    .cachedCount(6)
    .width('100%')
    .height('100%')
    .border({
      width: 0,
      color: Color.White
    })
    .onScrollIndex((first: number) => {
      console.info(`${TAG} onScrollIndex ${first}`)
      this.scrollIndex = first
      if (first === 0) {
        this.scrollerForGrid.scrollToIndex(0)
      }
    })
  }
}
??想了解更多關(guān)于開源的內(nèi)容,請(qǐng)?jiān)L問:??
??51CTO 開源基礎(chǔ)軟件社區(qū)??
??https://ost.51cto.com??