HarmonyOS 应用开发基础案例(十四):简易版影视APP

20 阅读7分钟

1. 案例效果

2. 资源初始化和资源文件

2.1. string.json (en_US)

{
  "string": [
    {
      "name": "module_desc",
      "value": "module description"
    },
    {
      "name": "EntryAbility_desc",
      "value": "description"
    },
    {
      "name": "EntryAbility_label",
      "value": "Use of Swiper"
    },
    {
      "name": "recently",
      "value": "Recent Plays"
    },
    {
      "name": "photo",
      "value": "camera"
    },
    {
      "name": "more",
      "value": "more >"
    },
    {
      "name": "movie_classic",
      "value": "Selected Films"
    },
    {
      "name": "lately",
      "value": "latest"
    },
    {
      "name": "like",
      "value": "like"
    },
    {
      "name": "comment",
      "value": "comment"
    },
    {
      "name": "share",
      "value": "share"
    },
    {
      "name": "movie",
      "value": "movie"
    },
    {
      "name": "movie_description_1",
      "value": "@HarmonyOS Official website"
    },
    {
      "name": "movie_description_2",
      "value": "#HarmonyOS Huawei Developer Conference"
    },
    {
      "name": "TV",
      "value": "TV"
    },
    {
      "name": "game",
      "value": "Game"
    },
    {
      "name": "live",
      "value": "Live"
    },
    {
      "name": "entertainment",
      "value": "Entertainment"
    }
  ]
}

2.2. string.json (zh_CN)

{
  "string": [
    {
      "name": "module_desc",
      "value": "module description"
    },
    {
      "name": "EntryAbility_desc",
      "value": "description"
    },
    {
      "name": "EntryAbility_label",
      "value": "Swiper的使用"
    },
    {
      "name": "recently",
      "value": "最近播放"
    },
    {
      "name": "photo",
      "value": "相机"
    },
    {
      "name": "more",
      "value": "更多 >"
    },
    {
      "name": "movie_classic",
      "value": "电影精选"
    },
    {
      "name": "lately",
      "value": "最新"
    },
    {
      "name": "like",
      "value": "点赞"
    },
    {
      "name": "comment",
      "value": "评论"
    },
    {
      "name": "share",
      "value": "转发"
    },
    {
      "name": "movie",
      "value": "视频"
    },
    {
      "name": "movie_description_1",
      "value": "@HarmonyOS 官网"
    },
    {
      "name": "movie_description_2",
      "value": "#HarmonyOS HDC大会"
    },
    {
      "name": "TV",
      "value": "电视剧"
    },
    {
      "name": "game",
      "value": "游戏"
    },
    {
      "name": "live",
      "value": "直播"
    },
    {
      "name": "entertainment",
      "value": "综艺"
    }
  ]
}

2.3. constants

  • CommonConstants
// ets/common/constants/CommonConstants.ets

export class CommonConstants {
  static readonly DURATION_PAGE = 50
  static readonly DURATION_ADS = 200
  static readonly HEIGHT_HEAD = 40
  static readonly HEIGHT_CAROUSEL_TITLE = 90
  static readonly FONT_SIZE_DESCRIPTION = 12
  static readonly FONT_SIZE_PHOTO_NAME = 14
  static readonly FONT_SIZE_SORT_TITLE = 16
  static readonly FONT_SIZE_UNCHECKED = 18
  static readonly FONT_SIZE_TITLE = 20
  static readonly FONT_SIZE_CHECKED = 24
  static readonly FONT_SIZE_PAGE_CONTENT = 28
  static readonly FONT_WEIGHT_LIGHT = 400
  static readonly FONT_WEIGHT_NORMAL = 500
  static readonly FONT_WEIGHT_BOLD = 700
  static readonly LAYOUT_WEIGHT = 1
  static readonly BORDER_RADIUS = 12
  static readonly LINE_HEIGHT_MORE = 19
  static readonly LINE_HEIGHT_NAVIGATION = 28
  static readonly SPACE_TOP_BAR = 16
  static readonly SPACE_NAVIGATION = 8
  static readonly WIDTH_HEAD_BORDER = 2
  static readonly WIDTH_HEAD = 40
  static readonly RADIUS_HEAD = 20
  static readonly SWIPER_TIME = 1500
  static readonly MARGIN_PLAY_PAGE = 10
  static readonly BOTTOM_TEXT = 4
  static readonly TOP_ADS = 12
  static readonly LEFT_POSITION = '3%'
  static readonly ADS_LEFT = 12
  static readonly TOP_NAME = 8
  static readonly TOP_DESCRIPTION = 4
  static readonly TOP_IMAGE_VOTE = 20
  static readonly TOP_HEAD = 40
  static readonly FULL_WIDTH = '100%'
  static readonly FULL_HEIGHT = '100%'
  static readonly WIDTH_PLAY = '95%'
  static readonly PAGE_WIDTH = '94.4%'
  static readonly WIDTH_SORT_NAME = '62.2%'
  static readonly WIDTH_SORT = '92%'
  static readonly WIDTH_MOVIE_SORT = '90%'
  static readonly WIDTH_PICTURE = '72%'
  static readonly HEIGHT_BANNER = '27%'
  static readonly WIDTH_VOTE = '8.9%'
  static readonly WIDTH_BACK_ICON = '6.7%'
  static readonly MARGIN_TOP_SORT = '3.2%'
  static readonly MARGIN_BOTTOM_SORT = '1.7%'
  static readonly MARGIN_BOTTOM_GRID = '4.2%'
  static readonly WIDTH_VIDEO = '26.2%'
  static readonly TWO_COLUMNS = '1fr 1fr'
  static readonly TWO_ROWS = '1fr 1fr'
  static readonly THREE_COLUMNS = '1fr 1fr 1fr'
  static readonly THREE_ROWS = '1fr 1fr 1fr'
  static readonly GAP_COLUMNS = '2.2%'
  static readonly HEIGHT_GRID = '45%'
  static readonly HEIGHT_DESCRIPTION = '12.3%'
  static readonly TOP_BAR_HEIGHT = '7.2%'
  static readonly HEIGHT_COMMENT = '4.1%'
  static readonly HEIGHT_BACK_ICON = '3.1%'
  static readonly OFFSET_COMMENT_X = '-5%'
  static readonly OFFSET_COMMENT_Y = '10%'
  static readonly OFFSET_DESCRIPTION_Y = '45%'
  static readonly START_POSITION = '0%'
  static readonly PLAY_PAGE = 'pages/PageVideo'
  static readonly HOME_PAGE = 'pages/SwiperIndex'
}

3. 视频列表

3.1. 顶部导航

3.1.1. TobBar 组件
// ets/view/common/TopBar.ets

import { TopBarItem } from '../../viewmodel/TopBarItem'
import { initializeOnStartup } from '../../viewmodel/TopBarViewModel'
import { CommonConstants } from '../../common/constants/CommonConstants'

@Component
export struct TopBar {
  @Prop index: number = 0
  private tabArray: Array<TopBarItem> = initializeOnStartup()

  build() {
    Row({ space: CommonConstants.SPACE_TOP_BAR }) {
      ForEach(this.tabArray,
        (item: TopBarItem) => {
          Text(item.name)
            .fontSize(this.index === item.id ? CommonConstants.FONT_SIZE_CHECKED : CommonConstants.FONT_SIZE_UNCHECKED)
            .fontColor(Color.Black)
            .textAlign(TextAlign.Center)
            .fontWeight(this.index === item.id ? FontWeight.Bold : FontWeight.Regular)
        }, (item: TopBarItem) => JSON.stringify(item))
    }
    .margin({ left: CommonConstants.ADS_LEFT })
    .width(CommonConstants.FULL_WIDTH)
    .height(CommonConstants.TOP_BAR_HEIGHT)
  }
}
3.1.2. TopBar 数据源
// ets/viewmodel/TopBarViewModel.ets

import { TopBarItem } from './TopBarItem'
import { TOP_BAR_DATA } from '../common/constants/TopBarConstants'

export function initializeOnStartup(): Array<TopBarItem> {
  let tabDataArray: Array<TopBarItem> = []
  TOP_BAR_DATA.forEach((item: TopBarItem) => {
    tabDataArray.push(new TopBarItem(item.id, item.name))
  })
  return tabDataArray
}
// ets/common/constants/TopBarConstants.ets

import { TopBarItem } from '../../viewmodel/TopBarItem'

export const TOP_BAR_DATA: TopBarItem[] = [
  new TopBarItem(0, '全部'),
  new TopBarItem(1, '电影'),
  new TopBarItem(2, '电视剧'),
  new TopBarItem(3, '综艺'),
  new TopBarItem(4, '直播'),
  new TopBarItem(5, '游戏')
]
// ets/viewmodel/TopBarItem.ets

export class TopBarItem {
  id: number
  name: string

  constructor(id: number, name: string) {
    this.id = id
    this.name = name
  }
}

3.2. 全部分类内容页面

3.2.1. 全部分类组件
// ets/view/tabcontent/PageAll.ets

import { Banner } from '../common/Banner'
import { PictureSort } from '../all/PictureSort'
import { CommonConstants } from '../../common/constants/CommonConstants'
import { PictureType } from '../../common/constants/PictureConstants'

@Preview
@Component
export struct PageAll {
  build() {
    Scroll() {
      Column() {
        Banner()
        PictureSort({ initType: PictureType.RECENTLY })
        PictureSort({ initType: PictureType.PHOTO })
      }
      .width(CommonConstants.FULL_WIDTH)
    }
    
  }
}
3.2.2. 轮播图组件
// ets/view/common/Banner.ets

import { CommonConstants } from "../../common/constants/CommonConstants"

@Component
export struct Banner {
  build() {
    Column() {
      Text('swiper')
    }
    .width(CommonConstants.PAGE_WIDTH)
    .height(CommonConstants.HEIGHT_BANNER)
  }
}
3.2.3. 图片列表组件
// ets/view/all/PictureSort.ets

import { PictureItem } from '../../viewmodel/PictureItem'
import { initializePictures } from '../../viewmodel/PictureViewModel'
import { PictureView } from '../common/PictureView'
import { PictureType } from '../../common/constants/PictureConstants'
import { CommonConstants } from '../../common/constants/CommonConstants'

@Extend(Text)
function textStyle(fontSize: number, fontWeight: number) {
  .fontSize(fontSize)
  .fontWeight(fontWeight)
  .fontColor($r('app.color.font_black'))
}

@Component
export struct PictureSort {
  @State photos: Array<PictureItem> = []
  @State private sortName: Resource = $r('app.string.recently')
  private initType: string = ''

  aboutToAppear() {
    if (PictureType.RECENTLY === this.initType) {
      this.sortName = $r('app.string.recently')
      this.photos = initializePictures(PictureType.RECENTLY)
    } else {
      this.sortName = $r('app.string.photo');
      this.photos = initializePictures(PictureType.PHOTO)
    }
  }

  build() {
    Column() {
      Row() {
        Text(this.sortName)
          .width(CommonConstants.WIDTH_SORT_NAME)
          .textStyle(CommonConstants.FONT_SIZE_SORT_TITLE, CommonConstants.FONT_WEIGHT_NORMAL)
        Text($r('app.string.more'))
          .layoutWeight(CommonConstants.LAYOUT_WEIGHT)
          .textAlign(TextAlign.End)
          .textStyle(CommonConstants.FONT_SIZE_PHOTO_NAME, CommonConstants.FONT_WEIGHT_LIGHT)
          .lineHeight(CommonConstants.LINE_HEIGHT_MORE)
          .opacity($r('app.float.opacity_light'))
      }
      .width(CommonConstants.WIDTH_SORT)
      .margin({ top: CommonConstants.MARGIN_TOP_SORT, bottom: CommonConstants.MARGIN_BOTTOM_SORT })

      Grid() {
        ForEach(this.photos, (item: PictureItem) => {
          GridItem() {
            PictureView({ photos: item })
          }
        }, (item: PictureItem) => JSON.stringify(item))
      }
      .columnsTemplate(CommonConstants.TWO_COLUMNS)
      .rowsTemplate(CommonConstants.TWO_ROWS)
      .columnsGap(CommonConstants.GAP_COLUMNS)
      .rowsGap(CommonConstants.GAP_COLUMNS)
      .width(CommonConstants.PAGE_WIDTH)
      .height(CommonConstants.HEIGHT_GRID)
      .margin({ bottom: CommonConstants.MARGIN_BOTTOM_GRID })
    }
  }
}
3.2.4. 图片视图
// ets/view/common/PictureView.ets

import { PictureItem } from '../../viewmodel/PictureItem'
import { CommonConstants } from '../../common/constants/CommonConstants'

@Component
export struct PictureView {
  private photos: PictureItem = new PictureItem()

  build() {
    Column() {
      Image(this.photos.image).borderRadius(CommonConstants.BORDER_RADIUS)
        .height(CommonConstants.WIDTH_PICTURE)

      Text(this.photos.name).width(CommonConstants.PAGE_WIDTH)
        .fontSize(CommonConstants.FONT_SIZE_PHOTO_NAME)
        .fontWeight(CommonConstants.FONT_WEIGHT_NORMAL)
        .margin({ top: CommonConstants.TOP_NAME })

      Text(this.photos.description)
        .width(CommonConstants.PAGE_WIDTH)
        .fontSize(CommonConstants.FONT_SIZE_DESCRIPTION)
        .fontWeight(CommonConstants.FONT_WEIGHT_LIGHT)
        .opacity($r('app.float.opacity_light'))
        .margin({ top: CommonConstants.TOP_DESCRIPTION, bottom: CommonConstants.BOTTOM_TEXT })
    }
    .height(CommonConstants.FULL_HEIGHT)
  }
}
3.2.5. 图片视图模型
// ets/viewmodel/PictureViewModel.ets

import { PictureItem } from './PictureItem'
import { PICTURE_RECENTLY, PICTURE_PHOTO, PICTURE_LATEST, PICTURE_BANNER } from '../common/constants/PictureConstants'
import { PictureType } from '../common/constants/PictureConstants'

export function initializePictures(initType: string): Array<PictureItem> {
  let imageDataArray: Array<PictureItem> = []
  switch (initType) {
    case PictureType.BANNER:
      PICTURE_BANNER.forEach((item: PictureItem) => {
        imageDataArray.push(item)
      })
      break
    case PictureType.RECENTLY:
      PICTURE_RECENTLY.forEach((item: PictureItem) => {
        imageDataArray.push(item)
      })
      break
    case PictureType.PHOTO:
      PICTURE_PHOTO.forEach((item: PictureItem) => {
        imageDataArray.push(item)
      })
      break
    case PictureType.LATEST:
      PICTURE_LATEST.forEach((item: PictureItem) => {
        imageDataArray.push(item)
      })
      break
    default:
      break
  }
  return imageDataArray
}
3.2.6. 图片模型
export class PictureItem {
  id: string = ''
  name: string = ''
  description: string = ''
  image: Resource = $r('app.media.image1')
}
3.2.7. 图片类型和数据源
// ets/common/constants/PictureConstants.ets

import { PictureItem } from '../../viewmodel/PictureItem'

export const PICTURE_BANNER: PictureItem[] = [
  { id: '1', name: '怒海', description: '怒海波涛', image: $r('app.media.image1') },
  { id: '2', name: '大山深处', description: '大山深处感人的亲情之歌', image: $r('app.media.image2') },
  { id: '3', name: '荒漠', description: '荒漠的亲情之歌', image: $r('app.media.image3') }
]

export const PICTURE_RECENTLY: PictureItem[] = [
  { id: '1', name: '背影', description: '感人的亲情之歌', image: $r('app.media.recently1') },
  { id: '2', name: '废墟之上', description: '勇闯无人之境', image: $r('app.media.recently2') },
  { id: '3', name: '无根之人', description: '悬疑国产力作', image: $r('app.media.recently3') },
  { id: '4', name: '摩天轮', description: '每个人心中都有一个童话', image: $r('app.media.recently4') }
]

export const PICTURE_PHOTO: PictureItem[] = [
  { id: '1', name: '蓝·静', description: '用放大镜看世界', image: $r('app.media.photo1') },
  { id: '2', name: '花', description: '每个人心中都有一个童话', image: $r('app.media.photo2') },
  { id: '3', name: '无根之人', description: '悬疑国产力作', image: $r('app.media.recently3') },
  { id: '4', name: '摩天轮', description: '每个人心中都有一个童话', image: $r('app.media.recently4') }
]

export const PICTURE_LATEST: PictureItem[] = [
  { id: '1', name: '潮·设计大会', description: '国际设计大师分...', image: $r('app.media.movie1') },
  { id: '2', name: '食客', description: '味蕾爆炸', image: $r('app.media.movie2') },
  { id: '3', name: '绿野仙踪', description: '热带雨林的故事', image: $r('app.media.image3') },
  { id: '4', name: '塔', description: '2021最期待的电...', image: $r('app.media.movie4') },
  { id: '5', name: '微缩世界', description: '用放大镜看世界', image: $r('app.media.movie5') },
  { id: '6', name: '非常规接触', description: '少年的奇妙之旅', image: $r('app.media.movie6') },
  { id: '7', name: '绿野仙踪', description: '热带雨林的故事', image: $r('app.media.movie7') },
  { id: '8', name: '塔', description: '用放大镜看世界', image: $r('app.media.movie8') },
  { id: '9', name: '食客', description: '热带雨林的故事', image: $r('app.media.movie9') }
]

export enum PictureType {
  RECENTLY = 'recently',
  PHOTO = 'photo',
  LATEST = 'latest',
  BANNER = 'banner'
}

3.3. 电影分类页面

3.3.1. 电影分类组件
// ets/view/tabcontent/PageMovie.ets

import { Banner } from '../common/Banner'
import { MovieSort } from '../movie/MovieSort'
import { CommonConstants } from '../../common/constants/CommonConstants'

@Preview
@Component
export struct PageMovie {
  build() {
    Scroll() {
      Column() {
        Banner()
        MovieSort()
      }
      .width(CommonConstants.FULL_WIDTH)
    }
    .scrollable(ScrollDirection.Vertical).scrollBar(BarState.Off)
  }
}
3.3.2. 电影分类视图
// ets/view/movie/MovieSort.ets

import { PictureItem } from '../../viewmodel/PictureItem'
import { initializePictures } from '../../viewmodel/PictureViewModel'
import { PictureType } from '../../common/constants/PictureConstants'
import { PictureView } from '../common/PictureView'
import { CommonConstants } from '../../common/constants/CommonConstants'

@Component
export struct MovieSort {
  @State photos: Array<PictureItem> = initializePictures(PictureType.LATEST)

  build() {
    Column() {
      Text($r('app.string.lately'))
        .width(CommonConstants.WIDTH_SORT)
        .margin({ top: CommonConstants.MARGIN_TOP_SORT, bottom: CommonConstants.MARGIN_BOTTOM_SORT })
        .fontSize(CommonConstants.FONT_SIZE_SORT_TITLE)
        .fontWeight(CommonConstants.FONT_WEIGHT_NORMAL)
        .fontColor($r('app.color.font_black'))

      Grid() {
        ForEach(this.photos, (item: PictureItem) => {
          GridItem() {
            PictureView({ photos: item })
          }
        }, (item: PictureItem) => JSON.stringify(item))
      }
      .columnsTemplate(CommonConstants.THREE_COLUMNS)
      .rowsTemplate(CommonConstants.THREE_ROWS)
      .columnsGap(CommonConstants.GAP_COLUMNS)
      .rowsGap(CommonConstants.GAP_COLUMNS)
      .width(CommonConstants.PAGE_WIDTH)
      .height(CommonConstants.WIDTH_MOVIE_SORT)
      .margin({ bottom: CommonConstants.MARGIN_BOTTOM_GRID })
    }
  }
}

3.4. 其他分类页面

3.4.1. 电视剧组件
// ets/view/tabcontent/PageTV.ets

import { CommonConstants } from '../../common/constants/CommonConstants'

@Component
export struct PageTV {
  build() {
    Column() {
      Text($r('app.string.TV'))
        .height(CommonConstants.FULL_HEIGHT)
        .fontSize(CommonConstants.FONT_SIZE_PAGE_CONTENT)
        .fontWeight(CommonConstants.FONT_WEIGHT_LIGHT)
    }
    .width(CommonConstants.FULL_WIDTH)
    .height(CommonConstants.FULL_HEIGHT)
  }
}
3.4.2. 综艺组件
// ets/view/tabcontent/PageEntertainment.ets

import { CommonConstants } from '../../common/constants/CommonConstants'

@Component
export struct PageEntertainment {
  build() {
    Column() {
      Text($r('app.string.entertainment'))
        .height(CommonConstants.FULL_HEIGHT)
        .fontSize(CommonConstants.FONT_SIZE_PAGE_CONTENT)
        .fontWeight(CommonConstants.FONT_WEIGHT_LIGHT)
    }
    .width(CommonConstants.FULL_WIDTH)
    .height(CommonConstants.FULL_HEIGHT)
  }
}
3.4.3. 直播组件
// ets/view/tabcontent/PageLive.ets

import { CommonConstants } from '../../common/constants/CommonConstants'

@Component
export struct PageLive {
  build() {
    Column() {
      Text($r('app.string.live'))
        .height(CommonConstants.FULL_HEIGHT)
        .fontSize(CommonConstants.FONT_SIZE_PAGE_CONTENT)
        .fontWeight(CommonConstants.FONT_WEIGHT_LIGHT)
    }
    .width(CommonConstants.FULL_WIDTH)
    .height(CommonConstants.FULL_HEIGHT)
  }
}
3.4.4. 游戏组件
// ets/view/tabcontent/PageContent.ets

import { CommonConstants } from '../../common/constants/CommonConstants'

@Component
export struct PageGame {
  build() {
    Column() {
      Text($r('app.string.game'))
        .height(CommonConstants.FULL_HEIGHT)
        .fontSize(CommonConstants.FONT_SIZE_PAGE_CONTENT)
        .fontWeight(CommonConstants.FONT_WEIGHT_LIGHT)
    }
    .width(CommonConstants.FULL_WIDTH)
    .height(CommonConstants.FULL_HEIGHT)
  }
}

4. 轮播实现

4.1. 首页

// ets/pages/SwiperIndex.ets

import { CommonConstants } from '../common/constants/CommonConstants'
import { TopBar } from '../view/common/TobBar'
import { PageAll } from '../view/tabcontent/PageAll'
import { PageEntertainment } from '../view/tabcontent/PageEntertainment'
import { PageGame } from '../view/tabcontent/PageGame'
import { PageLive } from '../view/tabcontent/PageLive'
import { PageMovie } from '../view/tabcontent/PageMovie'
import { PageTV } from '../view/tabcontent/PageTV'

@Entry
@Component
struct SwiperIndex {
  @State index: number = 0

  build() {
    Flex({
      direction: FlexDirection.Column,
      alignItems: ItemAlign.Start
    }) {
      TopBar({ index: $index })
      Swiper() {
        PageAll()
        PageMovie()
        PageTV()
        PageEntertainment()
        PageLive()
        PageGame()
      }
      .index(this.index)
      .indicator(false)
      .loop(false)
      .duration(CommonConstants.DURATION_PAGE)
      .onChange((index: number) => {
        this.index = index
      })
    }
    .backgroundColor($r('app.color.start_window_background'))
  }
}

4.2. 修改 TabBar 组件

import { TopBarItem } from '../../viewmodel/TopBarItem'
import { initializeOnStartup } from '../../viewmodel/TopBarViewModel'
import { CommonConstants } from '../../common/constants/CommonConstants'

@Preview
@Component
export struct TopBar {
  // @Prop index: number = 0
  // 1. @Prop 改为 @Link
  @Link index: number
  private tabArray: Array<TopBarItem> = initializeOnStartup()

  build() {
    Row({ space: CommonConstants.SPACE_TOP_BAR }) {
      ForEach(this.tabArray,
        (item: TopBarItem) => {
          Text(item.name)
            .fontSize(this.index === item.id ? CommonConstants.FONT_SIZE_CHECKED : CommonConstants.FONT_SIZE_UNCHECKED)
            .fontColor(Color.Black)
            .textAlign(TextAlign.Center)
            .fontWeight(this.index === item.id ? FontWeight.Bold : FontWeight.Regular)
            
            // 2. 绑定事件,修改index,实现swiper切换
            .onClick(() => {
              this.index = item.id
            })
        }, (item: TopBarItem) => JSON.stringify(item))
    }
    .margin({ left: CommonConstants.ADS_LEFT })
    .width(CommonConstants.FULL_WIDTH)
    .height(CommonConstants.TOP_BAR_HEIGHT)
  }
}

4.3. 修改 Banner 组件

// ets/view/common/Banner.ets

import { PictureItem } from '../../viewmodel/PictureItem'
import { PictureType } from '../../common/constants/PictureConstants'
import { initializePictures, startPlay, stopPlay } from '../../viewmodel/PictureViewModel'
import { CommonConstants } from '../../common/constants/CommonConstants'

@Extend(Text)
function textStyle(fontSize: number, fontWeight: number) {
  .fontSize(fontSize)
  .fontColor($r('app.color.start_window_background'))
  .fontWeight(fontWeight)
}

@Component
export struct Banner {
  @State index: number = 0
  private imageArray: Array<PictureItem> = []
  private swiperController: SwiperController = new SwiperController()
  private dotIndicator: DotIndicator = new DotIndicator()

  aboutToAppear() {
    this.dotIndicator.selectedColor($r('app.color.start_window_background'));
    this.imageArray = initializePictures(PictureType.BANNER);
    startPlay(this.swiperController);
  }

  aboutToDisappear() {
    stopPlay()
  }

  build() {
    Swiper(this.swiperController) {
      ForEach(this.imageArray, (item: PictureItem) => {
        Stack({ alignContent: Alignment.TopStart }) {
          Image(item.image)
            .objectFit(ImageFit.Fill)
            .height(CommonConstants.FULL_HEIGHT)
            .width(CommonConstants.FULL_WIDTH)
            .borderRadius(CommonConstants.BORDER_RADIUS)
            .align(Alignment.Center)

          Column() {
            Text($r('app.string.movie_classic'))
              .textStyle(CommonConstants.FONT_SIZE_DESCRIPTION, CommonConstants.FONT_WEIGHT_LIGHT)
              .opacity($r('app.float.opacity_deep'))
              .margin({ bottom: CommonConstants.BOTTOM_TEXT })
            Text(item.name)
              .textStyle(CommonConstants.FONT_SIZE_TITLE, CommonConstants.FONT_WEIGHT_BOLD)
          }
          .alignItems(HorizontalAlign.Start)
          .height(CommonConstants.HEIGHT_CAROUSEL_TITLE)
          .margin({ top: CommonConstants.TOP_ADS, left: CommonConstants.ADS_LEFT })
        }
        .height(CommonConstants.FULL_HEIGHT)
        .width(CommonConstants.FULL_WIDTH)
      }, (item: PictureItem) => JSON.stringify(item))
    }
    .width(CommonConstants.PAGE_WIDTH)
    .height(CommonConstants.HEIGHT_BANNER)
    .index(this.index)
    .indicator(this.dotIndicator)
    .duration(CommonConstants.DURATION_ADS)
  }
}
// ets/viewmoel/PictureViewModel.ets

import { PictureItem } from './PictureItem'
import { PICTURE_RECENTLY, PICTURE_PHOTO, PICTURE_LATEST, PICTURE_BANNER } from '../common/constants/PictureConstants'
import { PictureType } from '../common/constants/PictureConstants'
import { CommonConstants } from '../common/constants/CommonConstants'

export function initializePictures(initType: string): Array<PictureItem> {
  let imageDataArray: Array<PictureItem> = []
  switch (initType) {
    case PictureType.BANNER:
      PICTURE_BANNER.forEach((item: PictureItem) => {
        imageDataArray.push(item)
      })
      break
    case PictureType.RECENTLY:
      PICTURE_RECENTLY.forEach((item: PictureItem) => {
        imageDataArray.push(item)
      })
      break
    case PictureType.PHOTO:
      PICTURE_PHOTO.forEach((item: PictureItem) => {
        imageDataArray.push(item)
      })
      break
    case PictureType.LATEST:
      PICTURE_LATEST.forEach((item: PictureItem) => {
        imageDataArray.push(item)
      })
      break
    default:
      break
  }
  return imageDataArray
}

// 添加swiper调度任务
let timerIds: number[] = []

export function startPlay(swiperController: SwiperController): void {
  let timerId = setInterval(() => {
    swiperController.showNext()
  }, CommonConstants.SWIPER_TIME)
  timerIds.push(timerId)
}

export function stopPlay(): void {
  timerIds.forEach((item: number) => {
    clearTimeout(item)
  })
}

4.4. 联调预览

去掉 PageAll、PageMovie、TabBar 等组件的 @Preview 装饰器,打开 SwiperIndex 开始预览。

5. 视频滑动播放

5.1. 在 Banner 组件上添加路由

// 1. 导入路由模块
import { router } from '@kit.ArkUI'

import { PictureItem } from '../../viewmodel/PictureItem'
import { PictureType } from '../../common/constants/PictureConstants'
import { initializePictures, startPlay, stopPlay } from '../../viewmodel/PictureViewModel'
import { CommonConstants } from '../../common/constants/CommonConstants'

@Extend(Text)
function textStyle(fontSize: number, fontWeight: number) {
  .fontSize(fontSize)
  .fontColor($r('app.color.start_window_background'))
  .fontWeight(fontWeight)
}

@Component
export struct Banner {
  @State index: number = 0
  private imageArray: Array<PictureItem> = []
  private swiperController: SwiperController = new SwiperController()
  private dotIndicator: DotIndicator = new DotIndicator()

  aboutToAppear() {
    this.dotIndicator.selectedColor($r('app.color.start_window_background'));
    this.imageArray = initializePictures(PictureType.BANNER);
    startPlay(this.swiperController);
  }

  aboutToDisappear() {
    stopPlay()
  }

  build() {
    Swiper(this.swiperController) {
      ForEach(this.imageArray, (item: PictureItem) => {
        Stack({ alignContent: Alignment.TopStart }) {
          Image(item.image)
            .objectFit(ImageFit.Fill)
            .height(CommonConstants.FULL_HEIGHT)
            .width(CommonConstants.FULL_WIDTH)
            .borderRadius(CommonConstants.BORDER_RADIUS)
            .align(Alignment.Center)

              // 2.添加路由导航
            .onClick(() => {
              router.pushUrl({ url: CommonConstants.PLAY_PAGE })
            })

          Column() {
            Text($r('app.string.movie_classic'))
              .textStyle(CommonConstants.FONT_SIZE_DESCRIPTION, CommonConstants.FONT_WEIGHT_LIGHT)
              .opacity($r('app.float.opacity_deep'))
              .margin({ bottom: CommonConstants.BOTTOM_TEXT })
            Text(item.name)
              .textStyle(CommonConstants.FONT_SIZE_TITLE, CommonConstants.FONT_WEIGHT_BOLD)
          }
          .alignItems(HorizontalAlign.Start)
          .height(CommonConstants.HEIGHT_CAROUSEL_TITLE)
          .margin({ top: CommonConstants.TOP_ADS, left: CommonConstants.ADS_LEFT })
        }
        .height(CommonConstants.FULL_HEIGHT)
        .width(CommonConstants.FULL_WIDTH)
      }, (item: PictureItem) => JSON.stringify(item))
    }
    .width(CommonConstants.PAGE_WIDTH)
    .height(CommonConstants.HEIGHT_BANNER)
    .index(this.index)
    .indicator(this.dotIndicator)
    .duration(CommonConstants.DURATION_ADS)
  }
}

5.2. 在图片视图上添加路由

// ets/view/common/PictureView.ets

import { router } from '@kit.ArkUI'
import { PictureItem } from '../../viewmodel/PictureItem'
import { CommonConstants } from '../../common/constants/CommonConstants'

@Component
export struct PictureView {
  private photos: PictureItem = new PictureItem()

  build() {
    Column() {
      Image(this.photos.image).borderRadius(CommonConstants.BORDER_RADIUS)
        .height(CommonConstants.WIDTH_PICTURE)
        .onClick(() => {
          router.pushUrl({ url: CommonConstants.PLAY_PAGE })
        })

      Text(this.photos.name).width(CommonConstants.PAGE_WIDTH)
        .fontSize(CommonConstants.FONT_SIZE_PHOTO_NAME)
        .fontWeight(CommonConstants.FONT_WEIGHT_NORMAL)
        .margin({ top: CommonConstants.TOP_NAME })

      Text(this.photos.description)
        .width(CommonConstants.PAGE_WIDTH)
        .fontSize(CommonConstants.FONT_SIZE_DESCRIPTION)
        .fontWeight(CommonConstants.FONT_WEIGHT_LIGHT)
        .opacity($r('app.float.opacity_light'))
        .margin({ top: CommonConstants.TOP_DESCRIPTION, bottom: CommonConstants.BOTTOM_TEXT })
    }
    .height(CommonConstants.FULL_HEIGHT)
  }
}

5.3. 视频播放首页

// ets/pages/PageVideo.ets

import { VideoItem } from '../viewmodel/VideoItem'
import { initializeOnStartup } from '../viewmodel/VideoViewModel'
import { PlayView } from '../view/play/PlayView'
import { CommonConstants } from '../common/constants/CommonConstants'

@Entry
@Component
struct PageVideo {
  @State videoArray: Array<VideoItem> = initializeOnStartup()
  @State index: number = 0
  @State pageShow: boolean = false

  build() {
    Column() {
      Swiper() {
        ForEach(this.videoArray, (item: VideoItem, index: number | undefined) => {
          PlayView({
            index: $index,
            pageShow: $pageShow,
            item: item,
            barPosition: index
          })
        }, (item: VideoItem) => JSON.stringify(item))
      }
      .width(CommonConstants.FULL_WIDTH)
      .height(CommonConstants.FULL_HEIGHT)
      .indicator(false)
      .loop(false)
      .vertical(true)
      .onChange((index: number) => {
        this.index = index
      })
    }
  }

  onPageShow(): void {
    this.pageShow = true
  }

  onPageHide(): void {
    this.pageShow = false
  }
}

5.4. 视图模型

5.4.1. 视频模型
// ets/viewmodel/VideoItem.ets

@Observed
export class VideoItem {
  id: string = ''
  src: Resource = $rawfile('video1.mp4')
  likesCount: number = 0
  isLikes: boolean = false
  commentCount: number = 102
  shareTimes: number = 666
}
5.4.2. 视频数据源
// ets/common/constants/VideoConstants.ets

import { VideoItem } from '../../viewmodel/VideoItem'

export const VIDEO_DATA: VideoItem[] = [
  {
    id: '1',
    src: $rawfile('video1.mp4'),
    likesCount: 0,
    isLikes: false,
    commentCount: 102,
    shareTimes: 666
  },
  {
    id: '2',
    src: $rawfile('video2.mp4'),
    likesCount: 8654,
    isLikes: true,
    commentCount: 0,
    shareTimes: 0
  }
]

export enum PlayState {
  STOP = 0,
  START = 1,
  PAUSE = 2
}
5.4.3. 视频视图模型
// ets/viewmodel/VideoViewModel.ets

import { VideoItem } from './VideoItem'
import { VIDEO_DATA } from '../common/constants/VideoConstants'

export function initializeOnStartup(): Array<VideoItem> {
  let videoDataArray: Array<VideoItem> = []
  VIDEO_DATA.forEach((item: VideoItem) => {
    videoDataArray.push(item)
  })
  return videoDataArray
}

5.5. 播放组件

5.5.1. 播放视图
// ets/view/play/PlayView.ets

import { VideoItem } from '../../viewmodel/VideoItem'
import { CommonConstants } from '../../common/constants/CommonConstants'
import { PlayState } from '../../common/constants/VideoConstants'
import { NavigationView } from './NavigationView'
import { CommentView } from './CommentView'
import { DescriptionView } from './DescriptionView'

@Component
export struct PlayView {
  private isShow: boolean = false
  @Link @Watch('needPageShow') index: number
  @Link @Watch('needPageShow') pageShow: boolean
  @State item: VideoItem = new VideoItem()
  private barPosition: number = 0
  @State private playState: number = PlayState.STOP
  private videoController: VideoController = new VideoController()

  build() {
    Stack({ alignContent: Alignment.End }) {
      Video({
        src: this.item.src,
        controller: this.videoController
      })
        .controls(false)
        .autoPlay(this.playState === PlayState.START ? true : false)
        .objectFit(ImageFit.Fill)
        .loop(true)
        .height(CommonConstants.WIDTH_VIDEO)
        .width(CommonConstants.FULL_WIDTH)
        .onClick(() => {
          if (this.playState === PlayState.START) {
            this.playState = PlayState.PAUSE
            this.videoController.pause()
          } else if (this.playState === PlayState.PAUSE) {
            this.playState = PlayState.START
            this.videoController.start()
          }
        })

      NavigationView()
      CommentView({ item: this.item })
      DescriptionView()
    }
    .backgroundColor(Color.Black)
    .width(CommonConstants.FULL_WIDTH)
    .height(CommonConstants.FULL_HEIGHT)
  }

  onPageSwiperShow(): void {
    if (this.playState != PlayState.START) {
      this.playState = PlayState.START
      this.videoController.start()
    }
  }

  onPageSwiperHide(): void {
    if (this.playState != PlayState.STOP) {
      this.playState = PlayState.STOP
      this.videoController.stop()
    }
  }

  needPageShow(): void {
    if (this.pageShow === true) {
      if (this.barPosition === this.index) {
        this.isShow = true
        this.onPageSwiperShow()
      } else {
        if (this.isShow === true) {
          this.isShow = false
          this.onPageSwiperHide()
        }
      }
    } else {
      this.onPageSwiperHide()
    }
  }
}
5.5.2. 导航视图
// ets/view/play/NavigationView.ets

import { CommonConstants } from '../../common/constants/CommonConstants'

@Component
export struct NavigationView {
  build() {
    Navigator({ target: CommonConstants.HOME_PAGE, type: NavigationType.Back }) {
      Row({ space: CommonConstants.SPACE_NAVIGATION }) {
        Image($r('app.media.ic_back'))
          .width(CommonConstants.WIDTH_BACK_ICON)
          .height(CommonConstants.HEIGHT_BACK_ICON)
          .objectFit(ImageFit.Contain)

        Text($r('app.string.movie'))
          .fontSize(CommonConstants.FONT_SIZE_TITLE)
          .fontWeight(CommonConstants.FONT_WEIGHT_BOLD)
          .fontColor($r('app.color.start_window_background'))
          .textAlign(TextAlign.Center)
          .margin(CommonConstants.MARGIN_PLAY_PAGE)
          .lineHeight(CommonConstants.LINE_HEIGHT_NAVIGATION)
      }
    }
    .position({ x: CommonConstants.LEFT_POSITION, y: CommonConstants.START_POSITION })
  }
}
5.5.3. 互动视图
// ets/view/play/CommentView.ets

import { VideoItem } from '../../viewmodel/VideoItem';
import { CommonConstants } from '../../common/constants/CommonConstants'

@Extend(Text) function textStyle(fontSize: number, fonWeight: number) {
  .fontSize(fontSize)
  .fontWeight(fonWeight)
  .fontColor($r('app.color.start_window_background'))
  .textAlign(TextAlign.Center)
}

@Component
export struct CommentView {
  @ObjectLink item: VideoItem

  build() {
    Column() {
      Image($r('app.media.head'))
        .width(CommonConstants.WIDTH_HEAD)
        .height(CommonConstants.HEIGHT_HEAD)
        .margin({ top: CommonConstants.TOP_HEAD })
        .objectFit(ImageFit.Contain)
        .border({
          width: CommonConstants.WIDTH_HEAD_BORDER,
          color: Color.White,
          radius: CommonConstants.RADIUS_HEAD
        })

      Image(this.item.isLikes ? $r('app.media.vote1') : $r('app.media.vote0'))
        .width(CommonConstants.WIDTH_VOTE)
        .height(CommonConstants.HEIGHT_COMMENT)
        .onClick(() => {
          if (this.item.isLikes) {
            this.item.likesCount--
          } else {
            this.item.likesCount++
          }
          this.item.isLikes = !this.item.isLikes;
        })
        .margin({ top: CommonConstants.TOP_IMAGE_VOTE })

      Text(this.item.likesCount === 0 ? $r('app.string.like') : (this.item.likesCount.toString()))
        .textStyle(CommonConstants.FONT_SIZE_DESCRIPTION, CommonConstants.FONT_WEIGHT_LIGHT)

      Image($r('app.media.comment'))
        .width(CommonConstants.WIDTH_VOTE)
        .height(CommonConstants.HEIGHT_COMMENT)
        .margin({ top: CommonConstants.TOP_IMAGE_VOTE })
      Text(this.item.commentCount === 0 ? $r('app.string.comment') : (this.item.commentCount.toString()))
        .textStyle(CommonConstants.FONT_SIZE_DESCRIPTION, CommonConstants.FONT_WEIGHT_LIGHT)

      Image($r('app.media.share'))
        .width(CommonConstants.WIDTH_VOTE)
        .height(CommonConstants.HEIGHT_COMMENT)
        .margin({ top: CommonConstants.TOP_IMAGE_VOTE })
      Text(this.item.shareTimes === 0 ? $r('app.string.share') : (this.item.shareTimes.toString()))
        .textStyle(CommonConstants.FONT_SIZE_DESCRIPTION, CommonConstants.FONT_WEIGHT_LIGHT)
    }
    .offset({ x: CommonConstants.OFFSET_COMMENT_X, y: CommonConstants.OFFSET_COMMENT_Y })
  }
}
5.5.4. 描述视图
// ets/view/play/DescriptionView.ets

import { CommonConstants } from '../../common/constants/CommonConstants'

@Extend(Text) function textStyle(fontSize: number, fonWeight: number) {
  .fontSize(fontSize)
  .fontWeight(fonWeight)
  .fontColor($r('app.color.start_window_background'))
  .textAlign(TextAlign.Center)
  .margin(CommonConstants.MARGIN_PLAY_PAGE)
}

@Component
export struct DescriptionView {
  build() {
    Column() {
      Text($r('app.string.movie_description_1'))
        .textStyle(CommonConstants.FONT_SIZE_SORT_TITLE, CommonConstants.FONT_WEIGHT_NORMAL)
      Text($r('app.string.movie_description_2'))
        .textStyle(CommonConstants.FONT_SIZE_PHOTO_NAME, CommonConstants.FONT_WEIGHT_LIGHT)
        .opacity($r('app.float.opacity_deep'))
    }
    .height(CommonConstants.HEIGHT_DESCRIPTION)
    .width(CommonConstants.WIDTH_PLAY)
    .alignItems(HorizontalAlign.Start)
    .offset({ y: CommonConstants.OFFSET_DESCRIPTION_Y })
  }
}

✋ 需要参加鸿蒙认证的请点击 鸿蒙认证链接