#创作者激励#OpenHarmony仿视频播放器应用-爱电影(三) 原创 精华

NL_AIDC_XJS
发布于 2023-3-23 14:48
浏览
2收藏

【本文正在参加2023年第一期优质创作者激励计划】
作者:徐金生

仿视频播放器应用-爱电影合集

效果

在线视频

接上一篇,主页上显示了电影资源,点击你想看的电影会跳转至电影播放页面,接下来我们详细的说说电影播放页面开发涉及的内容,首先我们来看下电影播放页面的设计图,如下:

#创作者激励#OpenHarmony仿视频播放器应用-爱电影(三)-鸿蒙开发者社区#创作者激励#OpenHarmony仿视频播放器应用-爱电影(三)-鸿蒙开发者社区

从上图我们知道,从结构上来讲可以分为上下两部分组成,上部分是视频播放器,下部分是电影简介。
视频播放器:由前后两层,底层是视频播放,顶层是视频播放控制器,包括了返回按键、显示视频名称、控制视频的播放、暂停、更新进度、全屏显示、视频总时长和当前播放视频时间点。
电影简介:包括电影的介绍以及一些推荐的电影,点击“简介”弹窗显示该电影的详细信息,包括:电影的类型、来源、评分、热度、演员、详细的剧情等。

项目开发

开发环境

硬件平台:DAYU2000 RK3568
系统版本: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

程序代码

1、Playback.ets

首先我们看下视频播放页面的代码


import { VideoView } from '../view/VideoView';
import { PLAYBACK_SPEED, PLAYBACK_STATE } from '../model/Playback'
import router from '@ohos.router';
import { VideoListView } from '../view/VideoListView'
import { VideoData } from '../model/VideoData'
import { MockVideoData } from '../model/MockVideoData'
import { VideoDataUtils } from '../utils/VideoDataUtils'
import { VideoIntroduceView } from '../view/VideoIntroduceView'
import { VideoSpeed } from '../model/VideoSpeed'
import emitter from '@ohos.events.emitter';
import { CommonData } from '../model/CommonData'
/**
 * 视频播放页面
 */
const TAG: string = 'Playback'
@Entry
@Component
struct Playback {
  @State mTag: string = TAG
  private name: string
  private introduction: string
  @State uri: any = null
  @State previewImage: any = null
  private actors: string | Resource
  private directs: string | Resource
  @State rateIndex: number = 1
  @State rate: VideoSpeed = PLAYBACK_SPEED[1]
  @State @Watch('scrollChange') scrollIndex: number = 0
  @State likeVideoList: Array<VideoData> = []
  private scrollerForScroll: Scroller = new Scroller()
  @State isShowIntroduce: boolean = false // 是否显示简介
  @State mVideoData: VideoData = null
  @State isScrollClose: boolean = false
  @Provide('play_time') curTime: number = 0
  @State videoState: string = PLAYBACK_STATE.INIT
  @Provide('show_operation') isShowOperation: boolean = true
  aboutToAppear() {
    console.info(`${TAG} aboutToAppear curTime:${this.curTime}`)
    this.initData()
  }
  initData() {
    this.likeVideoList = MockVideoData.getVideoList()
    // 获取当前需要播放的电影资源信息
    this.mVideoData = router.getParams()['video_data']
    this.name = this.mVideoData.name
    this.uri = this.mVideoData.uri
    this.previewImage = this.mVideoData.image
    this.actors = VideoDataUtils.getUser(this.mVideoData.actors)
    this.directs = VideoDataUtils.getUser(this.mVideoData.directs)
    this.introduction = this.mVideoData.introduction
  }
  onCloseIntroduce() {
    this.isShowIntroduce = false
  }
  onScreen(isFull: boolean) {
    console.info(`${TAG} onScreen ${isFull} mVideoData:${JSON.stringify(this.mVideoData)} curTime:${this.curTime} videoState:${this.videoState}`)
    if (isFull) {
      router.pushUrl({
        url: 'pages/FullScreen',
        params: {
          video_data: this.mVideoData,
          cur_time: this.curTime, // 当前播放时间
          video_state: this.videoState // 播放状态
        }
      })
    }
  }
  scrollChange() {
    if (this.scrollIndex === 0) {
      this.scrollToAnimation(0, 0)
    } else if (this.scrollIndex === 2) {
      this.scrollToAnimation(0, 280)
    }
  }
  onPageShow() {
    // 竖屏显示
    emitter.emit({
      eventId: CommonData.EVENT_WINDOW_PORTRAIT_ID
    })
  }
  onPageHide() {
    console.info(`${TAG} onPageHide`)
  }
  scrollToAnimation(xOffset, yOffset) {
    this.scrollerForScroll.scrollTo({
      xOffset: xOffset,
      yOffset: yOffset,
      animation: {
        duration: 3000,
        curve: Curve.FastOutSlowIn
      }
    })
  }
  build() {
    Stack() {
      Column() {
        Stack({
          alignContent: Alignment.TopStart
        }) {
          VideoView({
            _TAG: this.mTag,
            videoUri: $uri,
            previewUri: $previewImage,
            videoRate: $rate,
            videoRateIndex: $rateIndex,
            onScreen: this.onScreen.bind(this),
            isFullScreen: false,
            videoState: $videoState,
            isEvent: true,
            mWidth: '100%',
            mHeight: '100%'
          })
            .margin({
              top: 15,
              bottom: 15
            })
          if (this.isShowOperation) {
            Row({ space: 10 }) {
              Image($r('app.media.icon_back'))
                .width(24)
                .height(24)
                .objectFit(ImageFit.Cover)
                .onClick(() => {
                  router.back()
                })
              Text(this.name)
                .fontSize(20)
                .fontColor(Color.White)
            }
            .padding(20)
          }
        }.width('100%')
        .height('40%')
        .backgroundColor(Color.Black)
        // 介绍
        Column() {
          Scroll(this.scrollerForScroll) {
            Column() {
              // 简介内容
              Column() {
                // 标题
                Row() {
                  Text(this.name)
                    .fontColor(Color.Black)
                    .fontSize(26)
                    .width('88%')
                  Row() {
                    Text($r('app.string.introduce'))
                      .fontColor($r('app.color.introduce_text'))
                      .fontSize(16)
                    Image($r('app.media.icon_right'))
                      .width(16)
                      .height(20)
                      .objectFit(ImageFit.Contain)
                  }.onClick(() => {
                    console.info(`CLICK 设置前 isShowIntroduce ${this.isShowIntroduce}`)
                    this.isScrollClose = false
                    this.isShowIntroduce = true
                    console.info(`CLICK 设置后 isShowIntroduce ${this.isShowIntroduce}`)
                  })
                }
                .justifyContent(FlexAlign.Start)
                .alignItems(VerticalAlign.Bottom)
                .width('100%')
                .height('40')
                .border({
                  width: 0,
                  color: Color.Gray
                })
                // 简介
                Column() {
                  Row({ space: 15 }) {
                    Text($r('app.string.directs'))
                      .fontColor($r('app.color.introduce_title_text'))
                      .fontSize(16)
                    Text(this.directs)
                      .fontColor($r('app.color.introduce_text'))
                      .fontSize(14)
                  }.justifyContent(FlexAlign.Start)
                  .width('100%')
                  .margin({
                    top: 5,
                    bottom: 5
                  })
                  Text($r('app.string.actors'))
                    .fontColor($r('app.color.introduce_title_text'))
                    .fontSize(16)
                    .margin({
                      top: 5,
                      bottom: 5
                    })
                    .width('100%')
                  Text(this.actors)
                    .fontColor($r('app.color.introduce_text'))
                    .fontSize(14)
                    .width('100%')
                    .margin({
                      top: 5,
                      bottom: 5
                    })
                  Text(this.introduction)
                    .fontColor($r('app.color.introduce_text'))
                    .fontSize(16)
                    .width('100%')
                    .lineHeight(26)
                    .maxLines(2)
                    .textOverflow({
                      overflow: TextOverflow.Ellipsis
                    })
                    .margin({
                      top: 5,
                      bottom: 5
                    })
                }
                .width('100%')
                .height(150)
                .justifyContent(FlexAlign.Start)
                .margin({
                  top: 20,
                  bottom: 20
                })
                .border({
                  width: 0,
                  color: Color.Green
                })
                Text($r('app.string.guess_like'))
                  .fontColor(Color.Black)
                  .fontSize(18)
                  .width('100%')
              }
              .width('100%')
              .height('280')
              .border({
                width: 0,
                color: Color.Red
              })
              // 猜你喜欢
              VideoListView({
                videoList: $likeVideoList,
                scrollIndex: $scrollIndex,
                isBlackModule: true
              })
            }
          }
          .scrollBar(BarState.Off)
        }
        .width('95%')
        .height('55%')
        .backgroundColor(Color.White)
        .padding(20)
        .margin(30)
        .border({
          radius: 20
        })
      }.width('100%')
      .height('100%')
      // 电影简介弹窗
      Panel(this.isShowIntroduce) {
        VideoIntroduceView({
          videoData: $mVideoData,
          onClose: this.onCloseIntroduce.bind(this),
          isScrollClose: this.isScrollClose
        })
      }
      .type(PanelType.Foldable) //  内容永久展示
      .mode(PanelMode.Half)
      .dragBar(false)
      .halfHeight(500)
      .onChange((width, height, mode) => {
        console.info(`${TAG} Panel onChange ${JSON.stringify(mode)}`)
        if (mode === PanelMode.Mini) {
          this.isShowIntroduce = false
          this.isScrollClose = true
        }
      })
    }
    .width('100%')
    .height('100%')
    .backgroundImage($r('app.media.main_bg'), ImageRepeat.XY)
  }
}

  • VideoView:抽象出的视频播放组件,用于播放视频和控制视频
  • 电影简介,外层使用Scroll封装,其中包含了简介的基础信息和推荐电影列表,由于外层是Scroll,电影推荐列表使用了Grid组件,存在滑动冲突的问题,解决方案参看:OpenHarmony仿视频播放器应用-爱电影(二) 中的 “
    问题1:Scroll与Grid列表嵌套时,电影列表无法显示完整,或者无法显示banner”。
  • 电影简介详情使用Panel容器封装,Panel为可滑动面板,显示时从底部向上滑起,类似于抽屉组件。
  • VideoIntroduceView:自定义封装的一个电影介绍的容器,容器内部包括了详细的电影简介。
  • VideoListView:猜你喜欢的模块中,使用了电影列表控件,这个在上一篇也有提到。
2、VideoView.ets

视频播放器组件,主要使用Video媒体容器组件实现,Video不仅可以加载本地资源,也可以加载网络资源,在加载网络资源时,首先需要在module.json5中添加ohos.permission.INTERNET权限,并且连接外网,然后只需要替换Video的src属性值为网络地址即可。


/**
 * 视频播放器视图
 */
import { PLAYBACK_SPEED, PLAYBACK_STATE } from '../model/Playback'
import { TimeUtils } from '../utils/TimeUtils'
import { VideoSpeed } from '../model/VideoSpeed'
import emitter from '@ohos.events.emitter';
import { CommonData } from '../model/CommonData'
const TAG: string = 'VideoView'
@Component
export struct VideoView {
  @Prop _TAG: string
  @Link videoUri: any
  @Link previewUri: any
  @Link videoRate: VideoSpeed
  @Link videoRateIndex: number
  @Prop mWidth: string
  @Prop mHeight: string
  private videoController: VideoController = new VideoController()
  @Link videoState: string
  @Consume('play_time') curTime: number
  @State curTimeStr: string = '00:00:00'
  @State durationCountStr: string = '00:00:00'
  @State curSliderValue: number = 0
  private durationNumber: number = 0
  private selectSpeedOption: Array<SelectOption>
  onScreen: (isFull: boolean) => void
  @Prop isFullScreen: boolean
  @State isAutoPlay: boolean = false // 是否自动播放
  private lastTime: number
  isEvent: boolean // 是否需要注册事件
  @Consume('show_operation') isShowOperation: boolean // 是否显示操作视图
  private timeID: number
  aboutToAppear() {
    console.info(`${TAG} ${this._TAG} aboutToAppear`)
    if (this.isEvent) {
      this.registerEmitter()
    }
    this.startTime()
    // 初始化播放倍数
    this.selectSpeedOption = new Array<SelectOption>()
    for (const item of PLAYBACK_SPEED) {
      let option: SelectOption = {
        value: item.val
      }
      this.selectSpeedOption.push(option)
    }
    this.updateVideo(this.curTime, this.videoState, 'aboutToAppear')
  }
  updateVideo(time: number, state: string, tag: string) {
    console.info(`${TAG} ${this._TAG} ${tag} updateVideo time: ${time} state: ${state}`)
    if (state === PLAYBACK_STATE.START) {
      console.info(`${TAG} ${this._TAG}  updateVideo start`)
      this.isAutoPlay = true
      this.lastTime = time
      this.vSetTime(time)
      //      this.videoController.start()
    } else {
      console.info(`${TAG} ${this._TAG}  updateVideo stop`)
      this.isAutoPlay = false
      this.videoController.stop()
    }
  }
  aboutToDisappear() {
    console.info(`${TAG} ${this._TAG}  aboutToDisappear`)
    this.destroy()
  }
  registerEmitter() {
    emitter.on({
      eventId: CommonData.EVENT_PLAY_VIDEO
    }, (event) => {
      console.info(`${TAG} ${this._TAG} ${CommonData.EVENT_PLAY_VIDEO} callback : ${JSON.stringify(event)}`)
      let params = event.data
      if (params.hasOwnProperty('cur_time')) {
        this.lastTime = params['cur_time']
        console.info(`${TAG} ${this._TAG} Emitter getParams curTime: ${this.curTime}`)
      }
      if (params.hasOwnProperty('video_state')) {
        this.videoState = params['video_state']
        console.info(`${TAG} ${this._TAG} Emitter getParams curTime: ${this.videoState}`)
      }
      console.info(`${TAG} ${this._TAG} Emitter getParams curTime: ${this.videoState}`)
      this.updateVideo(this.lastTime, this.videoState, 'emitter')
    })
  }
  unregisterEmitter() {
    emitter.off(CommonData.EVENT_PLAY_VIDEO)
  }
  vSetTime(time: number) {
    console.info(`${TAG} ${this._TAG}  vSetTime curTime: ${time}`)
    this.videoController.setCurrentTime(time, SeekMode.Accurate)
  }
  clickStartOrPause() {
    if (this.videoState === PLAYBACK_STATE.START) {
      this.videoController.pause()
    } else {
      this.videoController.start()
    }
  }
  startTime() {
    if (this.timeID > 0) {
      this.stopTime()
    }
    this.timeID = setTimeout(() => {
      this.isShowOperation = false
    }, 5000)
  }
  stopTime() {
    clearTimeout(this.timeID)
    this.timeID = -1
  }
  destroy() {
    this.videoController.stop()
    if (this.isEvent) {
      this.unregisterEmitter()
    }
    this.stopTime()
  }
  build() {
    Stack({
      alignContent: Alignment.BottomStart
    }) {
      // 视频播放
      Video({
        src: 'https://vd4.bdstatic.com/mda-jdmyw860sqcu8utw/sc/mda-jdmyw860sqcu8utw.mp4',
        previewUri: this.previewUri,
        currentProgressRate: this.videoRate.speed,
        controller: this.videoController
      })
        .width('100%')
        .backgroundColor('#000000')
        .controls(false)
        .autoPlay(this.isAutoPlay)
        .objectFit(ImageFit.Contain)
        .onTouch((event) => {
          if (event.type === TouchType.Down) {
            console.info(`${TAG} ${this._TAG} 视频被点击`)
            this.isShowOperation = !this.isShowOperation
            if (this.isShowOperation) {
              this.startTime()
            }
          }
        })
        .onStart(() => {
          console.info(`${TAG} ${this._TAG} 播放`)
          this.videoState = PLAYBACK_STATE.START
        })
        .onPause(() => {
          console.info(`${TAG} ${this._TAG} 暂停`)
          this.videoState = PLAYBACK_STATE.PAUSE
        })
        .onFinish(() => {
          console.info(`${TAG} ${this._TAG} 结束`)
          this.videoState = PLAYBACK_STATE.FINISH
        })
        .onError(() => {
          console.info(`${TAG} ${this._TAG} 播放失败`)
          this.videoState = PLAYBACK_STATE.ERROR
        })
        .onPrepared((callback) => {
          // 	视频准备完成时触发该事件,通过duration可以获取视频时长,单位为秒(s)
          this.durationNumber = callback.duration
          this.durationCountStr = TimeUtils.FormatTime(this.durationNumber)
          //          console.info(`${TAG} onPrepared 视频时长 ${this.durationCountStr} 原始值:${this.durationNumber}`)
        })
        .onSeeking((callback) => {
          // 操作进度条过程时上报时间信息,单位为s。
          console.info(`${TAG} ${this._TAG} onSeeking ${callback.time}`)
        })
        .onUpdate((callback) => {
          // 播放进度变化时触发该事件,单位为s,更新时间间隔为250ms。
          if (this.lastTime > 0 && callback.time !== this.lastTime) {
            console.info(`${TAG} ${this._TAG} onUpdate vSetTime curTime lastTime ${this.lastTime} callback time:${callback.time}`)
            this.vSetTime(this.lastTime)
            this.curTime = this.lastTime
            this.lastTime = 0
          } else {
            this.curTime = callback.time
            console.info(`${TAG} ${this._TAG} onUpdate curTime ${this.curTime}`)
          }
          this.curTimeStr = TimeUtils.FormatTime(this.curTime)
          //          console.info(`${TAG} onUpdate 视频播放时间更新 ${this.curTimeStr} 原始值${callback.time}`)
          this.curSliderValue = TimeUtils.Rounding(this.curTime * 100 / this.durationNumber)
          //          console.info(`${TAG} onUpdate 更新滑块进度 ${this.curSliderValue}`)
        })
        .onFullscreenChange((callback) => {
          // 在全屏播放与非全屏播放状态之间切换时触发该事件,返回值为true表示进入全屏播放状态,为false则表示非全屏播放。
          console.info(`${TAG} ${this._TAG} onFullscreenChange ${callback.fullscreen}`)
        })
      if (this.isShowOperation) {
        // 居中操作按钮(播放/暂停)
        Column() {
          Image(this.videoState !== PLAYBACK_STATE.START ? $r('app.media.video_start_60') : $r('app.media.video_pause_60'))
            .width(60)
            .height(60)
            .objectFit(ImageFit.Cover)
            .onClick(() => {
              this.clickStartOrPause()
            })
        }
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
        .width('100%')
        .height('100%')
        // 视频操作栏
        Row({
          space: 10
        }) {
          // 播放/暂停按钮
          Image(this.videoState !== PLAYBACK_STATE.START ? $r('app.media.video_start') : $r('app.media.video_pause'))
            .width(26)
            .height(26)
            .objectFit(ImageFit.Cover)
            .onClick(() => {
              this.clickStartOrPause()
            })
          // 播放时间
          Text(this.curTimeStr)
            .fontSize(10)
            .fontColor(Color.White)
          // 进度条
          Slider({
            value: this.curSliderValue,
            min: 0,
            max: 100,
            style: SliderStyle.OutSet
          })
            .showSteps(false)
            .showTips(false)
            .blockColor(Color.White)
            .trackColor(Color.White)
            .selectedColor('#36AD08')
            .width('50%')
            .onChange((value: number, mode: SliderChangeMode) => {
              // SliderChangeMode Begin=0:开始 Moving=1 End=2 Click=3
              let timePercentage = value.toFixed(0)
              console.info(`${TAG} Slider onChange ${value} ${timePercentage}`)
              // 计算滑动滑块需要播放的时间
              this.curTime = parseInt(timePercentage) * this.durationNumber / 100
              this.vSetTime(this.curTime)
              this.curTimeStr = TimeUtils.FormatTime(this.curTime)
              console.info(`${TAG} ${this._TAG}  Slider onChange 滑块滑动时间变更 curTime:${this.curTime} curTimeStr ${this.curTimeStr}`)
            })
          // 总时长
          Text(this.durationCountStr)
            .fontSize(10)
            .fontColor(Color.White)
          Blank()
          // 播放倍数
          if (this.isFullScreen) {
            Select(this.selectSpeedOption)
              .selected(this.videoRateIndex)
              .value(this.videoRate.val)
              .font({ size: 10 })
              .fontColor(Color.White)
              .selectedOptionFont({ size: 10 })
              .selectedOptionFontColor('#F54F02')
              .optionFontColor('#5E5E5E')
              .optionFont({ size: 10 })
              .onSelect((index: number) => {
                console.info('Select:' + index)
                this.videoRate = PLAYBACK_SPEED[index]
                this.videoRateIndex = index
                console.info(`${TAG} videoRateIndex = ${this.videoRateIndex}`)
              })
              .border({
                width: 0,
                color: Color.White
              })
          }
          // 浮动层
          Image($r('app.media.icon_float'))
            .width(32)
            .height(32)
            .objectFit(ImageFit.Cover)
            .onClick(() => {
              console.info(`${TAG} 启动浮动层`)
            })
          // 全屏切换
          Image(this.isFullScreen ? $r('app.media.icon_small_screen') : $r('app.media.icon_full_screen'))
            .width(26)
            .height(26)
            .objectFit(ImageFit.Cover)
            .onClick(() => {
              console.info(`${TAG} 全屏切换`)
              this.onScreen(!this.isFullScreen)
            })
        }
        .width('100%')
        .height('60')
        .backgroundImage($r('app.media.bg_control_1'), ImageRepeat.X)
        .padding({
          left: 20,
          right: 20
        })
      }
    }.width(this.mWidth)
    .height(this.mHeight)
    .backgroundColor(Color.Gray)
  }
}

下面对以上代码进行说明:

  • @Provide() @Consume() :这里的注解@Provide和@Consume,两者需要配合使用。@Provide作为数据的提供方,可以更新其子孙节点的数据,并触发页面渲染。@Consume在感知到@Provide数据的更新后,会触发当前自定义组件的重新渲染。
    | @Consume(‘play_time’) curTime:用于向全屏播放时同步当前播放的时间节点,在全屏播放时可以继续播放。
    | @Consume(‘show_operation’) isShowOperation: 用于控制播放器上层视图(暂停、播放、进度、时间、最大化)的显示和隐藏。

  • Video中有onStart、onPause、onFinish、onError、onPrepared、onSeeking、onUpdate 函数用于监听视频播放的状态

  • Slider:滑动条组件,用于显示和控制视频播放的进度,用户可以移动滑块来控制视频播放的进度。

  • Select:下拉选择菜单,用于选择视频的倍数,在全屏播放时使用。

3、VideoIntroduceView.ets

详细的电影介绍,包括电影的相关信息,演员列表,演员图片、电影的剧情。


import { VideoData, User } from '../model/VideoData'
/**
 * 电影简介
 */
const TAG: string = 'VideoIntroduceView'
@Component
export struct VideoIntroduceView {
  @Link videoData: VideoData
  onClose: () => void
  @Prop @Watch('scrollClose') isScrollClose: boolean //  是否为滚动关闭
  private userList: Array<User> = []
  private scroller: Scroller = new Scroller()
  aboutToAppear() {
    console.info(`${TAG} aboutToAppear`)
    // 初始化演员数据
    if (this.videoData) {
      // 先添加导演
      for (const item of this.videoData.directs) {
        this.userList.push(item)
      }
      // 再添加演员
      for (const item of this.videoData.actors) {
        this.userList.push(item)
      }
    }
  }
  scrollClose() {
    if (this.isScrollClose) {
      this.scroller.scrollEdge(Edge.Top)
    }
  }
  aboutToDisappear() {
    this.userList = []
  }
  build() {
    Scroll(this.scroller) {
      Column() {
        Row() {
          // 简介标题
          Text($r('app.string.introduce'))
            .fontSize(22)
            .fontColor(Color.Black)
            .width('95%')
          Column() {
            Image($r('app.media.icon_close'))
              .width(22)
              .height(22)
              .objectFit(ImageFit.Cover)
          }.width(32)
          .height(32)
          .onClick(() => {
            console.info(`${TAG} CLOSE onClose`)
            this.scroller.scrollEdge(Edge.Top)
            this.onClose()
          })
        }.justifyContent(FlexAlign.Start)
        .width('100%')
        .margin({
          top: 10,
          bottom: 10
        })
        // 电影基础信息
        Row({ space: 20 }) {
          Image(this.videoData.image)
            .width(105)
            .height(135)
            .objectFit(ImageFit.Cover)
          Column({ space: 10 }) {
            Text(this.videoData.name)
              .fontSize(18)
              .fontColor('#2A1818')
              .align(Alignment.Start)
              .width('300')
            Row() {
              Text(this.videoData.describe)
                .fontSize(14)
                .fontColor(Color.White)
                .backgroundColor('#C4C4C4')
                .padding({
                  top: 5,
                  bottom: 5,
                  left: 10,
                  right: 10
                })
              Blank()
            }.width('300')
            Text(this.videoData.resourceType)
              .fontSize(18)
              .fontColor('#5E5E5E')
              .width(300)
            Row({ space: 5 }) {
              Image($r('app.media.icon_source'))
                .width(16)
                .height(16)
                .objectFit(ImageFit.Cover)
              Text(this.videoData.source)
                .fontSize(18)
                .fontColor('#5E5E5E')
                .width(200)
            }
            .width(300)
            .alignItems(VerticalAlign.Center)
            .justifyContent(FlexAlign.Start)
          }
          .justifyContent(FlexAlign.Start)
        }
        .width('100%')
        .height(135)
        .justifyContent(FlexAlign.Start)
        .margin({
          top: 10,
          bottom: 10
        })
        // 评分
        Row({ space: 20 }) {
          Column({ space: 0 }) {
            Text(this.videoData.grade)
              .fontSize(35)
              .fontColor('#F54F02')
            Text(this.videoData.gradeNumber + '人参与评分')
              .fontSize(12)
              .fontColor('#868686')
          }.width('120')
          Row({ space: 10 }) {
            Image($r('app.media.icon_fire'))
              .width(16)
              .height(16)
              .objectFit(ImageFit.Cover)
            Text($r('app.string.heat_value'))
              .fontSize(14)
              .fontColor('#F54F02')
            Progress({
              value: this.videoData.heat,
              type: ProgressType.Linear,
              total: 100
            })
              .width('40%')
              .color('#F54F02')
              .backgroundColor('#868686')
              .style({
                strokeWidth: 10
              })
          }
        }
        .width('100%')
        .justifyContent(FlexAlign.Start)
        .margin({
          top: 10,
          bottom: 10
        })
        // 演职员
        Text($r('app.string.cast'))
          .fontSize(18)
          .fontColor('#2A1818')
          .width('100%')
          .margin({
            top: 10,
            bottom: 10
          })
        // 演员列表
        Row() {
          List({
            space: 10,
            initialIndex: 0
          }) {
            ForEach(this.userList, (item: User) => {
              ListItem() {
                Column({ space: 10 }) {
                  Image(item.icon)
                    .width(105)
                    .height(135)
                    .objectFit(ImageFit.Cover)
                  Text(item.name)
                    .width(105)
                    .fontSize(14)
                    .fontColor('#2A1818')
                  Text(item.role)
                    .width(105)
                    .fontSize(12)
                    .fontColor('#868686')
                }
              }
            })
          }
          .scrollBar(BarState.Off)
          .listDirection(Axis.Horizontal)
          .cachedCount(3)
        }.width('100%')
        .justifyContent(FlexAlign.Start)
        .margin({
          top: 10,
          bottom: 10
        })
        // 剧情
        Text($r('app.string.story'))
          .fontSize(18)
          .fontColor('#2A1818')
          .width('100%')
          .margin({
            top: 10,
            bottom: 10
          })
        Text(this.videoData.introduction)
          .fontSize(12)
          .fontColor('#868686')
          .margin({
            top: 10,
            bottom: 10
          })
      }
      .padding({
        top: 20,
        bottom: 20,
        left: 40,
        right: 40
      })
      .justifyContent(FlexAlign.Start)
    }.width('100%')
    .scrollBar(BarState.Off)
    .backgroundColor(Color.White)
    .border({
      radius: 20
    })
  }
}

电影简介中并无比较复杂的内容,主要是根据UI设计将各组件进行布置即可,这里主要讲讲电影简介外层的容器—Panel,可滑动面板,是一种用于内容展示的窗口,窗口的尺寸可以切换,根据PanelType的不同,可以分为三种不同类型的屏幕,大(类全屏)、中(类半屏)、小,具体说明如下:

PanelType枚举说明
名称 描述
Minibar 提供minibar和类全屏展示切换效果。
Foldable 内容永久展示类,提供大(类全屏)、中(类半屏)、小三种尺寸展示切换效果。
Temporary 内容临时展示区,提供大(类全屏)、中(类半屏)两种尺寸展示切换效果。

可滑动面板的初始状态有三种:

PanelMode枚举说明
名称 描述
Mini 类型为minibar和foldable时,为最小状态;类型为temporary,则不生效。
Half 类型为foldable和temporary时,为类半屏状态;类型为minibar,则不生效。
Full 类全屏状态。

在这里Panel的type为PanelType.Foldable,mode为PanelMode.Half,也就是初始为半屏显示,同时设置了半屏的高度halfHeight(500)。相关属性详解如下:

属性
名称 参数类型 描述
type PanelType 设置可滑动面板的类型。<br/>默认值:PanelType.Foldable
mode PanelMode 设置可滑动面板的初始状态。
dragBar boolean 设置是否存在dragbar,true表示存在,false表示不存在。<br/>默认值:true
fullHeight string | number 指定PanelMode.Full状态下的高度。
halfHeight string | number 指定PanelMode.Half状态下的高度,默认为屏幕尺寸的一半。
miniHeight string | number 指定PanelMode.Mini状态下的高度。
show boolean 当滑动面板弹出时调用。
backgroundMask<sup>9+</sup> ResourceColor 指定Panel的背景蒙层。

Panel组件向下滑动的时候会进入PanelModeMini状态,此状态中滑块不会完全消失在界面上,而是需要动态的设置Panel构造函数来控制组件的显隐。

Panel(show:boolean)

参数:

参数名 参数类型 必填 参数描述
show boolean 控制Panel显示或隐藏。

那我们如何指导Panel的变化呢,主要是通过Panel的监听事件onChange(),通过回调函数中的PanelMode参数判断当前Panel的状态。

事件
名称 功能描述
onChange(event: (width: number, height: number, mode: PanelMode) => void) 当可滑动面板发生状态变化时触发, 返回的height值为内容区高度值,当dragbar属性为true时,panel本身的高度值为dragbar高度加上内容区高度。

这里整个视频播放的主要内容就讲解完了,下一篇我们继续讲解视频全屏播放页面的实现。

感谢

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

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2023-3-23 16:57:12修改
4
收藏 2
回复
举报
3条回复
按时间正序
/
按时间倒序
红叶亦知秋
红叶亦知秋

赞,对代码有说明就读起来很舒服

已于2023-3-23 15:29:38修改
回复
2023-3-23 15:29:33
殇时云起
殇时云起

这个UI就很清爽

回复
2023-3-24 11:16:23
晒太阳的懒猫
晒太阳的懒猫

学习、收藏。喜欢这种编程风格。简洁!

回复
2023-3-25 11:56:51
回复
    相关推荐