鸿蒙简易版影视APP案例实战

发布于:2025-06-04 ⋅ 阅读:(28) ⋅ 点赞:(0)

目录

1. 案例效果

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

2.1. string.json (en_US)

2.2. string.json (zh_CN)

2.3. constants

3. 视频列表

3.1. 顶部导航

3.1.1. TobBar 组件

3.1.2. TopBar 数据源

3.2. 全部分类内容页面

3.2.1. 全部分类组件

3.2.2. 轮播图组件

3.2.3. 图片列表组件

3.2.4. 图片视图

3.2.5. 图片视图模型

3.2.6. 图片模型

3.2.7. 图片类型和数据源

3.3. 电影分类页面

3.3.1. 电影分类组件

3.3.2. 电影分类视图

3.4. 其他分类页面

3.4.1. 电视剧组件

3.4.2. 综艺组件

3.4.3. 直播组件

3.4.4. 游戏组件

4. 轮播实现

4.1. 首页

4.2. 修改 TabBar 组件

4.3. 修改 Banner 组件

4.4. 联调预览

5. 视频滑动播放

5.1. 在 Banner 组件上添加路由

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

5.3. 视频播放首页

5.4. 视图模型

5.4.1. 视频模型

5.4.2. 视频数据源

5.4.3. 视频视图模型

5.5. 播放组件

5.5.1. 播放视图

5.5.2. 导航视图

5.5.3. 互动视图

5.5.4. 描述视图


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 })
  }
}

配套视频:《简易版影视APP案例实战》


网站公告

今日签到

点亮在社区的每一天
去签到