视频播放
原创大约 9 分钟
定义视频信息
在src/main/ets/model/
目录创建VideoInfo.ets
视频信息类,内容如下。
/**
* 视频信息类
*
*/
export class VideoInfo {
// 视频ID
videoId: number = 0;
// 视频文件路径
videoPath: string = "";
// 作者
author: string = "";
// 头像文件路径
profilePath: string = "";
// 视频封面文件路径
coverPath: string = "";
// 内容
content: string = "";
// 点赞数量
thumbsUpCount: number = 0;
// 评论数量
commentCount: number = 0;
// 收藏数量
favoriteCount: number = 0;
// 分享数量
shareCount: number = 0;
// 是否关注
isFollow: boolean = false;
// 是否点赞
isThumbsUp: boolean = false;
// 是否收藏
isFavorite: boolean = false;
}
在src/main/ets/common/
目录创建PlayState.ets
播放状态枚举类,内容如下。
/**
* 播放状态枚举类
*
*/
export enum PlayState {
STOP = 0,
START = 1,
PAUSE = 2
}
在src/main/ets/common/Constant.ets
文件中增加视频信息常量数据。
/**
* 常量类
*
*/
export default class Constant {
......
/**
* 视频数据
*/
static readonly VIDEO_INFO_ARRAY: Array<VideoInfo> = [
{
"videoId": 1,
"coverPath": "video/video_cover_01.jpg",
"videoPath": "video/video_01.mp4",
"author": "科技UP主",
"profilePath": "profile/profile_01.jpg",
"content": "视频一视频一视频一视频一视频一视频一视频一视频一视频一视频一视频一视频一视频一视频一视频一视频一视频一视频一视频一视频一",
"thumbsUpCount": 11,
"commentCount": 27,
"favoriteCount": 33,
"shareCount": 9,
"isFollow": false,
"isThumbsUp": false,
"isFavorite": false
},
{
"videoId": 2,
"coverPath": "video/video_cover_02.jpg",
"videoPath": "video/video_02.mp4",
"author": "生活UP主",
"profilePath": "profile/profile_02.jpg",
"content": "视频二视频二视频二视频二视频二视频二视频二视频二视频二视频二视频二视频二视频二视频二视频二视频二视频二视频二视频二视频二",
"thumbsUpCount": 54,
"commentCount": 24,
"favoriteCount": 36,
"shareCount": 39,
"isFollow": false,
"isThumbsUp": false,
"isFavorite": false
},
{
"videoId": 3,
"coverPath": "video/video_cover_03.jpg",
"videoPath": "video/video_03.mp4",
"author": "时势UP主",
"profilePath": "profile/profile_03.jpg",
"content": "视频三视频三视频三视频三视频三视频三视频三视频三视频三视频三视频三视频三视频三视频三视频三视频三视频三视频三视频三视频三",
"thumbsUpCount": 7,
"commentCount": 3,
"favoriteCount": 81,
"shareCount": 95,
"isFollow": false,
"isThumbsUp": false,
"isFavorite": false
},
{
"videoId": 4,
"coverPath": "video/video_cover_04.jpg",
"videoPath": "video/video_04.mp4",
"author": "人文UP主",
"profilePath": "profile/profile_04.jpg",
"content": "视频四视频四视频四视频四视频四视频四视频四视频四视频四视频四视频四视频四视频四视频四视频四视频四视频四视频四视频四视频四",
"thumbsUpCount": 22,
"commentCount": 1,
"favoriteCount": 14,
"shareCount": 4,
"isFollow": false,
"isThumbsUp": false,
"isFavorite": false
},
{
"videoId": 5,
"coverPath": "video/video_cover_05.jpg",
"videoPath": "video/video_05.mp4",
"author": "历史UP主",
"profilePath": "profile/profile_05.jpg",
"content": "视频五视频五视频五视频五视频五视频五视频五视频五视频五视频五视频五视频五视频五视频五视频五视频五视频五视频五视频五视频五",
"thumbsUpCount": 1,
"commentCount": 2,
"favoriteCount": 3,
"shareCount": 7,
"isFollow": false,
"isThumbsUp": false,
"isFavorite": false
}
];
}
创建视频布局
在src/main/ets/pages/
目录中创建VideoPlayer.ets
文件,内容如下。
/**
* 视频播放器
*
*/
@Component
export struct VideoPlayer {
private controller: VideoController = new VideoController();
build() {
}
}
创建src/main/ets/view/
目录,并依次创建视频整体界面布局VideoView.ets
文件、视频分类标签组件VideoClassificationLabel.ets
、视频信息展示区VideoInfoDisplay.ets
和视频侧边栏组件VideoSideBar.ets
。
import { PlayState } from "../common/PlayState";
import { VideoInfo } from "../model/VideoInfo";
import { VideoClassificationLabel } from "./VideoClassificationLabel";
import { VideoInfoDisplay } from "./VideoInfoDisplay";
import { VideoSideBar } from "./VideoSideBar";
/**
* 视频播放界面
*
*/
@Component
export struct VideoView {
// 视频源
private videoInfo: VideoInfo = new VideoInfo();
// 视频控制器
private videoController: VideoController = new VideoController();
// 播放状态
@State
private playState: number = PlayState.STOP;
// 播放某个视频的时候,其他的视频内容都需要停止播放
// Swiper滑动时显示的那一页的索引
@Link
@Watch('handlePageShow')
index: number;
@Link
@Watch('handlePageShow')
isShow: boolean;
// 当前视频在数组中的索引位置
currentIndex: number = 0;
// 组件即将出现时执行
aboutToAppear(): void {
this.handlePageShow();
}
build() {
// 层叠布局
Stack() {
// 播放器
Video({
src: $rawfile(this.videoInfo.videoPath),
previewUri: $rawfile(this.videoInfo.coverPath),
controller: this.videoController
})
.controls(true)
.autoPlay(this.playState === PlayState.START ? true : false)
// 视频适配模式
.objectFit(ImageFit.Contain)
.loop(true)
.width('100%')
.height('100%')
// 设置点击事件
.onClick(() => this.handleOnClick())
// 添加播放按钮
Image($r('app.media.video_play'))
.width(50)
.height(50)
.visibility(this.playState === PlayState.START ? Visibility.Hidden : Visibility.Visible)
.onClick(() => this.handleOnClick())
// 视频分类
VideoClassificationLabel()
// 绝对定位
.position({x: 0, y: 20})
// 信息展示
VideoInfoDisplay({ videoInfo: this.videoInfo })
// 相对定位
.offset({ x: '0%', y: '36%' })
// 侧边栏
VideoSideBar({ videoInfo: this.videoInfo })
// 绝对定位
.position({x: '87%', y: '33%'})
}
.backgroundColor(Color.Black)
.width('100%')
.height('100%')
}
/**
* 自定义播放方法
*
*/
play(): void {
// 修改播放状态为START
if (this.playState != PlayState.START) {
this.playState = PlayState.START;
}
// 通过控制器来播放
this.videoController.start();
}
/**
* 自定义暂停方法
*
*/
pause(): void {
// 修改播放状态为START
if (this.playState != PlayState.PAUSE) {
this.playState = PlayState.PAUSE;
}
// 通过控制器来暂停
this.videoController.pause();
}
/**
* 自定义停止方法
*
*/
stop(): void {
// 修改播放状态为START
if (this.playState != PlayState.STOP) {
this.playState = PlayState.STOP;
}
// 通过控制器来播放
this.videoController.stop();
}
/**
* 自定义点击事件
*/
handleOnClick(): void {
// 如果是播放,点击则暂停
if (this.playState == PlayState.START) {
this.pause();
}
// 如果是暂停,点击则播放
else if (this.playState == PlayState.PAUSE || this.playState == PlayState.STOP) {
this.play();
}
}
/**
* 根据index、isShow和currentIndex来修改播放器的状态
*/
handlePageShow(): void {
// 判断视频界面是否进入前台播放,以及当前视频是否为Swiper选中的视频
if (this.isShow && this.currentIndex === this.index) {
this.play();
} else {
this.stop();
}
}
}
/**
* 分类标签界面
*
*/
@Component
export struct VideoClassificationLabel {
build() {
Row() {
// 主轴方向等间距
Flex({ justifyContent: FlexAlign.SpaceEvenly }) {
Text('直播')
.fontColor('#EEEEEE')
.fontSize(18);
Text('同城')
.fontColor('#EEEEEE')
.fontSize(18);
Text('关注')
.fontColor('#EEEEEE')
.fontSize(18);
Text('推荐')
.fontColor('#EEEEEE')
.fontSize(18);
Text('搜索')
.fontColor('#EEEEEE')
.fontSize(18);
}
}
.width('100%')
.height(50)
}
}
import { VideoInfo } from "../model/VideoInfo"
/**
* 视频信息展示组件
*
*/
@Component
export struct VideoInfoDisplay {
@State
videoInfo: VideoInfo = new VideoInfo();
build() {
// 主体布局
Column() {
// 用户信息
Row() {
// 用户头像
Image($rawfile(this.videoInfo.profilePath))
.width(60)
.height(60)
.borderRadius(30)
// 用户名
Text(this.videoInfo.author)
.margin({ left: 5 })
.fontSize(23)
.fontColor('#FFFFFF')
// 关注按钮
Text(this.videoInfo.isFollow ? '取关' : '关注')
.margin({ left: 5 })
.textAlign(TextAlign.Center)
.fontSize(16)
.fontColor('#FFFFFF')
.backgroundColor(this.videoInfo.isFollow ? Color.Gray : Color.Red)
.borderRadius(5)
.width(40)
.height(20)
}
.width('100%')
.height(70)
// 视频信息
Row() {
Text(this.videoInfo.content)
.fontSize(16)
.height(40)
.fontColor('#FFFFFF')
// 最多两行
.maxLines(2)
// 文本超长时用省略号代替
.textOverflow({ overflow: TextOverflow.Ellipsis})
}
.width('100%')
.height(40)
}
.width('100%')
.height(120)
.padding({ left: 5, right: 5, top: 5, bottom: 5 })
}
}
import { VideoInfo } from "../model/VideoInfo"
/**
* 视频侧边栏组件
*
*/
@Component
export struct VideoSideBar {
@State
videoInfo: VideoInfo = new VideoInfo();
build() {
Column() {
/**
* 点赞组件
*/
// 点赞按钮
Image(this.videoInfo.isThumbsUp ? $r('app.media.like_red') : $r('app.media.like'))
.width(40)
.height(40)
// 点赞数量
Text(this.videoInfo.thumbsUpCount + '')
.margin({ top: 10 })
.fontSize(12)
.fontColor('#FFFFFF')
.width(40)
.textAlign(TextAlign.Center)
/**
* 评论组件
*/
// 评论按钮
Image($r('app.media.comment'))
.width(40)
.height(40)
.margin({ top: 15 })
// 评论数量
Text(this.videoInfo.commentCount + '')
.margin({ top: 10 })
.fontSize(12)
.fontColor('#FFFFFF')
.width(40)
.textAlign(TextAlign.Center)
/**
* 收藏组件
*/
// 收藏按钮
Image(this.videoInfo.isFavorite ? $r('app.media.star_red') : $r('app.media.star'))
.width(40)
.height(40)
.margin({ top: 15 })
// 收藏数量
Text(this.videoInfo.favoriteCount + '')
.margin({ top: 10 })
.fontSize(12)
.fontColor('#FFFFFF')
.width(40)
.textAlign(TextAlign.Center)
/**
* 分享组件
*/
// 分享按钮
Image($r('app.media.share'))
.width(40)
.height(40)
.margin({ top: 15 })
// 分享数量
Text(this.videoInfo.shareCount + '')
.margin({ top: 10 })
.fontSize(12)
.fontColor('#FFFFFF')
.width(40)
.textAlign(TextAlign.Center)
}
.width(40)
.height(260)
}
}
定义视频数据源
创建src/main/ets/datasource/
目录并在其中创建抽象数据源类BaseDataSource.ets
和视频信息数据源VideoInfoDataSource.ets
。
/**
* 抽象数据源类
*
*/
export abstract class BaseDataSource<T> implements IDataSource {
// 定义监听器空数组
private listeners: Array<DataChangeListener> = [];
// 定义数据源数据
private dataSources: Array<T> = new Array<T>();
// 定义构造函数
constructor(dataSources: Array<T>) {
this.dataSources = dataSources;
}
/**
* 返回数据总条数
*
*/
totalCount(): number {
let error: Error | null = null;
try {
return this.dataSources == null ? 0 : this.dataSources.length;
} catch (e) {
error = e as Error;
throw new Error(error.message);
}
}
/**
* 获取指定索引的数据
*
*/
getData(index: number) {
let error: Error | null = null;
try {
if (index >= 0 && index <= this.totalCount()) {
return this.dataSources[index]
} else {
return null;
}
} catch (e) {
error = e as Error;
throw new Error(error.message);
}
}
/**
* 注册数据变化监听器
*
*/
registerDataChangeListener(listener: DataChangeListener): void {
let error: Error | null = null;
try {
// 当监听器不在监听器数组中时就添加
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
} catch (e) {
error = e as Error;
throw new Error(error.message);
}
}
/**
* 取消注册数据变化监听器
*
*/
unregisterDataChangeListener(listener: DataChangeListener): void {
let error: Error | null = null;
try {
// 当监听器在监听器数组中时就添加
let index: number = this.listeners.indexOf(listener);
if (index >= 0) {
this.listeners.splice(index, 1)
}
} catch (e) {
error = e as Error;
throw new Error(error.message);
}
}
/**
* 实现自定义方法:获取数据源
*
*/
getDataSource(): Array<T> {
return this.dataSources;
}
/**
* 实现自定义方法:添加数据
*
*/
add(index: number, data: T): void {
this.dataSources.splice(index, 0, data);
this.notifyDataAdd(index);
}
/**
* 实现自定义方法:添加数据
*
*/
push(data: T): void {
this.dataSources.push(data);
this.notifyDataAdd(this.dataSources.length - 1);
}
/**
* 实现自定义方法:删除数据
*
*/
remove(index: number): void {
this.dataSources.splice(index, 1)
this.notifyDataRemove(index);
}
/**
* 实现自定义方法:更新数据
*
*/
update(index: number, data: T): void {
this.dataSources.splice(index, 1, data)
this.notifyDataUpdate(index);
}
/**
* 实现自定义方法:重新加载数据
*
*/
reloadData(): void {
this.notifyDataReload();
}
/**
* 实现自定义方法:添加数据后发出通知
*
*/
notifyDataAdd(index: number) {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
})
}
/**
* 实现自定义方法:删除数据后发出通知
*
*/
notifyDataRemove(index: number) {
this.listeners.forEach(listener => {
listener.onDataDelete(index);
})
}
/**
* 实现自定义方法:更新数据后发出通知
*
*/
notifyDataUpdate(index: number) {
this.listeners.forEach(listener => {
listener.onDataChange(index);
})
}
/**
* 实现自定义方法:重新加载数据后发出通知
*
*/
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded();
})
}
}
import { VideoInfo } from '../model/VideoInfo';
import { BaseDataSource } from './BaseDataSource';
/**
* 视频信息数据源
*
*/
export class VideoInfoDataSource extends BaseDataSource<VideoInfo> {
constructor(videoArray: Array<VideoInfo>) {
super(videoArray);
}
}
页面数据传递
定义好了页面、数据以及各种组件之后,再将它们融合起来。
在src/main/ets/pages/
目录中创建Main.ets
、Me.ets
和VideoPublish.ets
文件,内容如下。
import { VideoInfoDataSource } from "../datasource/VideoInfoDataSource";
import { VideoInfo } from "../model/VideoInfo";
import { VideoView } from "../view/VideoView";
/**
* 首页
*
*/
@Component
export struct Main {
// Swiper滑动时当前播放的视频
@State
index: number = 0;
// 是否显示视频
@Link
isShow: boolean;
// 视频信息数据源
@Link
videoInfoDataSource: VideoInfoDataSource;
build() {
Column() {
Swiper() {
// 普通循环遍历
// ForEach(this.videoArray, (videoInfo:VideoInfo, position) => {
// 数据懒加载的循环遍历
LazyForEach(this.videoInfoDataSource, (videoInfo:VideoInfo, position) => {
VideoView({
videoInfo: videoInfo,
currentIndex: position,
index: this.index,
isShow: this.isShow
})
})
}
.width('100%')
.height('100%')
// 导航指示器
.indicator(false)
.loop(false)
// 纵向切换,也就是上下滑动而不是左右滑动
.vertical(true)
.onChange((count: number) => {
// 记录当前播放视频索引
this.index = count;
})
}.width("100%")
}
}
/**
* 我
*
*/
@Component
export struct Me {
@State message: string = "我";
build() {
Row() {
Column() {
Text(this.message).fontSize(38)
}.width("100%")
}.height("100%")
}
}
/**
* 视频发布
*
*/
@Component
export struct VideoPublish {
@State message: string = "视频发布";
build() {
Row() {
Column() {
Text(this.message).fontSize(38)
}.width("100%")
}.height("100%")
}
}
效果展示
最后修改src/main/ets/pages/
目录中的Index.ets
,内容如下。
import Constant from '../common/Constant';
import { VideoInfoDataSource } from '../datasource/VideoInfoDataSource';
import { Main } from './Main'
import { Me } from './Me'
import { VideoPublish } from './VideoPublish'
/**
* 主界面
*
*/
@Entry
@Component
struct Index {
// 是否显示视频界面
@State
isShow: boolean = false;
// 视频信息数据源
@State
videoInfoDataSource: VideoInfoDataSource = new VideoInfoDataSource(Constant.VIDEO_INFO_ARRAY);
// 当前Tabs页面的索引
@State
currentIndex: number = 0;
// 在其他生命周期处理isShow变量
aboutToAppear(): void {
this.isShow = true;
}
aboutToDisappear(): void {
this.isShow = false;
}
onPageShow(): void {
this.isShow = true;
}
onPageHide(): void {
this.isShow = false;
}
build() {
// 设置导航栏
Tabs({ barPosition: BarPosition.End }) {
// 各个导航的页签
TabContent() {
Main({
isShow: this.isShow,
videoInfoDataSource: this.videoInfoDataSource
})
}.tabBar(this.TabBuilder("首页", 0))
TabContent() {
VideoPublish()
}.tabBar(this.TabBuilder("发布", 1))
TabContent() {
Me()
}.tabBar(this.TabBuilder("我", 2))
}
// 判断当前页
.onChange((index: number) => {
this.currentIndex = index;
// 页面的索引从0开始
if (index === 0) {
this.isShow = true;
} else {
this.isShow = false;
}
})
}
/**
* 自定义TabBuilder导航栏组件
*
*/
@Builder
TabBuilder(title: string, index: number) {
Column() {
// 设置选中状态
Text(title)
.fontColor(this.currentIndex === index ? '#FFFFFF' : '#999999')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.Black)
}
}
最终的预览效果如下图所示。

感谢支持
更多内容,请移步《超级个体》。