#创作者激励#OpenHarmony仿视频播放器应用-爱电影(三) 原创 精华
【本文正在参加2023年第一期优质创作者激励计划】
作者:徐金生
仿视频播放器应用-爱电影合集
- OpenHarmony仿视频播放器应用-爱电影(一)
- OpenHarmony仿视频播放器应用-爱电影(二)
- 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高度加上内容区高度。 |
这里整个视频播放的主要内容就讲解完了,下一篇我们继续讲解视频全屏播放页面的实现。
感谢
如果您能看到最后,还希望您能动动手指点个赞,一个人能走多远关键在于与谁同行,我用跨越山海的一路相伴,希望得到您的点赞。
赞,对代码有说明就读起来很舒服
这个UI就很清爽
学习、收藏。喜欢这种编程风格。简洁!