HarmonyOS 鸿蒙Next 求一个类似抖音首页视频播放效果的demo
HarmonyOS 鸿蒙Next 求一个类似抖音首页视频播放效果的demo
//index.ets
import { media } from '@kit.MediaKit';
import { VideoPlayer } from '../component/VideoPlayer';
import { MyDataSource, MyDataSource1 } from '../model/MyDataSource';
import { BusinessError } from '@kit.BasicServicesKit';
const TAG = ‘[VideoPlayer]’;
@Entry
@Component
struct Index {
@State videoFiles: media.AVFileDescriptor[] = [];
@State sources: string[] = [
‘http://pic.jxxw.com.cn/v4/group6/M00/46/78/rBAVFGX7n8-AV-eqALs6PYM30QU262.mp4’,
‘http://pic.jxxw.com.cn/v4/group6/M00/46/E9/rBAVFGX8OAuAeQ02Ar2vUF4paaM297.mp4’,
‘http://pic.jxxw.com.cn/v4/group6/M00/46/E9/rBAVFGX8N6mAE4RTAfzVGt4a3q8950.mp4’,
‘http://pic.jxxw.com.cn/v4/group6/M00/46/E9/rBAVFGX8NyiAH-0GAzhCZIp9ov8315.mp4’,
‘http://pic.jxxw.com.cn/v4/group6/M00/46/DA/rBAVFGX8BOGAdpbeAZzE0OrluYE328.mp4’,
‘http://pic.jxxw.com.cn/v4/group6/M00/46/AF/rBAVFGX747yANuUQANxGPVX_AQs667.mp4’,
‘http://pic.jxxw.com.cn/v4/group6/M00/46/A8/rBAVFGX73eiANYqRAw2t0F21veQ341.mp4’,
‘http://pic.jxxw.com.cn/v4/group6/M00/46/78/rBAVFGX7n-2AaEoPAHxtdYWT1uE952.mp4’,
];
@State curIndex: number = 0;
@State firstFlag: boolean = true;
private swiperController: SwiperController = new SwiperController();
aboutToAppear(): void {
this.initFiles();
}
build() {
Swiper(this.swiperController) {
// LazyForEach(new MyDataSource(this.sources), (item: string, index: number) => {
LazyForEach(new MyDataSource1(this.videoFiles), (item: string, index: number) => {
VideoPlayer({ curSource: item, curIndex: this.curIndex, index: index, firstFlag: this.firstFlag })
}, (item: string, index: number) => JSON.stringify(item) + index)
}
.width(‘100%’)
.height(‘100%’)
.index($$this.curIndex)
.vertical(true)
.loop(false)
.indicator(false)
.backgroundColor(Color.Black)
.onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => {
console.info(TAG, onGestureSwipe index: ${index},extraInfo: ${<span class="hljs-built_in"><span class="hljs-built_in">JSON</span></span>.stringify(extraInfo)}.
)
})
.onAnimationStart((index: number, targetIndex: number, extraInfo: SwiperAnimationEvent) => {
console.info(TAG, onAnimationStart index: ${index},targetIndex: ${targetIndex},extraInfo: ${<span class="hljs-built_in"><span class="hljs-built_in">JSON</span></span>.stringify(extraInfo)}.
)
})
.onAnimationEnd((index: number, extraInfo: SwiperAnimationEvent) => {
console.info(TAG, onAnimationEnd index: ${index},extraInfo: ${<span class="hljs-built_in"><span class="hljs-built_in">JSON</span></span>.stringify(extraInfo)}.
)
})
}
initFiles() {
let fileList: string[] = getContext(this).resourceManager.getRawFileListSync(‘video’);
fileList.forEach((fileStr: string) => {
// 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址
// 返回类型为{fd,offset,length},fd为HAP包fd地址,offset为媒体资源偏移量,length为播放长度
let fileDescriptor = getContext().resourceManager.getRawFdSync(video/${fileStr}
);
let avFileDescriptor: media.AVFileDescriptor = {
fd: fileDescriptor.fd,
offset: fileDescriptor.offset,
length: fileDescriptor.length
};
this.videoFiles.push(avFileDescriptor)
})
}
}
//VideoPlayer.ets
import { media } from ‘@kit.MediaKit’;
import { BusinessError } from ‘@kit.BasicServicesKit’;
const TAG = ‘[VideoPlayer]’;
enum StateEnum {
idle = ‘idle’,
initialized = ‘initialized’,
prepared = ‘prepared’,
playing = ‘playing’,
paused = ‘paused’,
completed = ‘completed’,
stopped = ‘stopped’,
released = ‘released’,
error = ‘error’,
}
@Preview
@Component
export struct VideoPlayer {
@Prop @Watch(‘onIndexChange’) curIndex: number = -1;
private index: number = 0;
private curSource: media.AVFileDescriptor | string = ‘’;
@State isPlaying: boolean = true;
@State isOpacity: boolean = false;
@State flag: boolean = false;
@State currentTime: number = 0;
@State durationTime: number = 0;
@State durationStringTime: string = ‘00:00’;
@State currentStringTime: string = ‘00:00’;
private duration: number = 0;
private surfaceID: string = ‘’;
private xComponentController = new XComponentController();
private avPlayer: media.AVPlayer | undefined = undefined;
@Link firstFlag: boolean;
onIndexChange() {
console.info(TAG, enter onIndexChange. <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.curIndex:${<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.curIndex} <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.index:${<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.index}
)
if (this.curIndex !== this.index) {
this.pause();
this.isOpacity = false;
this.isPlaying = false;
} else {
if (this.flag === true) {
this.play();
this.isPlaying = true;
this.isOpacity = true;
} else {
// The scheduled task determines whether the video loading is complete.
let intervalFlag = setInterval(() => {
if (this.flag === true) {
this.play();
this.isPlaying = true;
this.isOpacity = true;
clearInterval(intervalFlag);
}
}, 100);
}
}
}
build() {
Column() {
Stack({ alignContent: Alignment.Center }) {
Stack() {
if (!this.isPlaying) {
Image($r(‘app.media.ic_public_play’))
.width(50)
.height(50)
.zIndex(2)
.onClick(() => {
this.iconOnclick();
});
}
Column() {
XComponent({
id: ‘’,
type: XComponentType.SURFACE,
libraryname: ‘’,
controller: this.xComponentController
})
.onLoad(() => {
this.xComponentController.setXComponentSurfaceSize({
surfaceWidth: 1920,
surfaceHeight: 1080
});
let surfaceID = this.xComponentController.getXComponentSurfaceId();
this.surfaceID = surfaceID;
this.initAVPlayer();
})
.width(‘100%’)
.height(‘100%’);
}
.zIndex(1)
.onClick(() => {
this.iconOnclick();
})
}
this.PlayControl()
}
.height(‘100%’)
.backgroundColor(Color.Black)
.width(‘100%’)
}
.width(‘100%’)
.height(‘100%’)
}
@Builder
PlayControl() {
Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
Image(this.isPlaying ? $r(‘app.media.ic_pause’) : $r(‘app.media.ic_play’))
.width(‘20vp’)
.height(‘20vp’)
.onClick(() => {
this.iconOnclick();
});
Text(this.currentStringTime)
.fontSize(‘14vp’)
.fontColor(Color.White)
.margin({ left: ‘2vp’ })
Slider({
value: this.currentTime,
step: 1,
min: 0,
max: this.durationTime,
style: SliderStyle.OutSet
})
.blockColor(Color.White)
.width(‘60%’)
.trackColor(Color.Gray)
.selectedColor(Color.White)
.showSteps(false)
.showTips(false)
.trackThickness(this.isOpacity ? 2 : 4)
.onChange((value: number, mode: SliderChangeMode) => {
this.sliderOnchange(value, mode);
});
Text(this.durationStringTime)
.fontSize(‘14vp’)
.fontColor(Color.White)
.margin({ left: ‘2vp’})
}
.zIndex(2)
.padding({ right: ‘2vp’ })
.opacity(this.isOpacity ? 0.5 : 1)
.offset({ x: 0, y: 350 })
.width(‘100%’)
.backgroundBlurStyle(BlurStyle.Thin, { colorMode: ThemeColorMode.DARK })
}
iconOnclick() {
if (this.isPlaying) {
this.pause();
this.isOpacity = false;
this.isPlaying = false;
return;
}
if (this.flag === true) {
this.play();
this.isPlaying = true;
this.isOpacity = true;
} else {
// The scheduled task determines whether the video loading is complete.
let intervalFlag = setInterval(() => {
if (this.flag === true) {
this.play();
this.isPlaying = true;
this.isOpacity = true;
clearInterval(intervalFlag);
}
}, 100);
}
}
sliderOnchange(value: number, mode: SliderChangeMode) {
console.info(TAG, sliderOnchange. value is ${value}
);
this.currentTime = value
if (mode === SliderChangeMode.Begin || mode === SliderChangeMode.Moving) {
this.isOpacity = false;
}
if (mode === SliderChangeMode.End) {
let seekTime: number = value * this.duration / this.durationTime;
this.currentStringTime = this.secondToTime(Math.floor(seekTime / 1000));
console.info(TAG, sliderOnchange. time is ${seekTime}, currentTime is ${<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.currentTime}
);
this.setSeek(seekTime);
this.isOpacity = true;
}
}
secondToTime(seconds: number): string {
let hourUnit = 60 * 60;
let hour: number = Math.floor(seconds / hourUnit);
let minute: number = Math.floor((seconds - hour * hourUnit) / 60);
let second: number = seconds - hour * hourUnit - minute * 60;
let hourStr: string = hour < 10 ? <span class="hljs-number"><span class="hljs-number">0</span></span>${hour.toString()}
: ${hour.toString()}
let minuteStr: string = minute < 10 ? <span class="hljs-number"><span class="hljs-number">0</span></span>${minute.toString()}
: ${minute.toString()}
let secondStr: string = second < 10 ? <span class="hljs-number"><span class="hljs-number">0</span></span>${second.toString()}
: ${second.toString()}
if (hour > 0) {
return ${hourStr}:${minuteStr}:${secondStr}
;
}
if (minute > 0) {
return ${minuteStr}:${secondStr}
;
} else {
return <span class="hljs-number"><span class="hljs-number">00</span></span>:${secondStr}
;
}
}
initAVPlayer() {
media.createAVPlayer().then((video: media.AVPlayer) => {
if (video != null) {
this.avPlayer = video;
this.setAVPlayerCallback(video);
// 设置播放源,使其进入initialized状态
if (typeof this.curSource === ‘string’) {
video.url = this.curSource;
} else {
video.fdSrc = this.curSource;
}
console.info(TAG, ‘createAVPlayer success’);
} else {
console.error(TAG, ‘createAVPlayer fail’);
}
}).catch((error: BusinessError) => {
console.error(TAG, AVPlayer catchCallback, error message:${error.message}
);
});
}
// 注册avplayer回调函数
setAVPlayerCallback(avPlayer: media.AVPlayer) {
avPlayer.on(‘timeUpdate’, (time: number) => {
// console.info(TAG, AVPlayer timeUpdate. time is ${time}
);
this.currentTime = Math.floor(time * this.durationTime / this.duration);
this.currentStringTime = this.secondToTime(Math.floor(time / 1000));
})
// seek操作结果回调函数
avPlayer.on(‘seekDone’, (seekDoneTime: number) => {
console.info(TAG, AVPlayer seekDone succeeded, seek time is ${seekDoneTime}
);
})
// 监听setSpeed生效的事件
avPlayer.on(‘speedDone’, (speed: number) => {
console.info(TAG, AVPlayer speedDone succeeded, speed is ${speed}
);
})
// error回调监听函数,当avPlayer在操作过程中出现错误时调用 reset接口触发重置流程
avPlayer.on(‘error’, (err: BusinessError) => {
console.error(TAG, Invoke avPlayer failed, code is ${err.code}, message is ${err.message}
);
avPlayer.reset(); // 调用reset重置资源,触发idle状态
})
// 状态机变化回调函数
avPlayer.on(‘stateChange’, async (state: string, reason: media.StateChangeReason) => {
switch (state) {
case ‘idle’: // 成功调用reset接口后触发该状态机上报
console.info(TAG, ‘AVPlayer state idle called.’);
// 设置播放源,使其进入initialized状态
if (typeof this.curSource === ‘string’) {
avPlayer.url = this.curSource;
} else {
avPlayer.fdSrc = this.curSource;
}
break;
case ‘initialized’: // avplayer 设置播放源后触发该状态上报
console.info(TAG, ‘AVPlayer state initialized called.’);
avPlayer.surfaceId = this.surfaceID;
avPlayer.prepare();
break;
case ‘prepared’: // prepare调用成功后上报该状态机
console.info(TAG, ‘AVPlayer state prepared called.’);
this.flag = true;
this.duration = avPlayer.duration;
this.durationTime = Math.floor(this.duration / 1000);
this.durationStringTime = this.secondToTime(this.durationTime);
if (this.index == 0 && this.firstFlag) {
avPlayer.play();
}
this.firstFlag = false;
// avPlayer.play();
break;
case ‘completed’: // prepare调用成功后上报该状态机
console.info(TAG, ‘AVPlayer state completed called.’);
this.isPlaying = false;
break;
case ‘playing’: // play成功调用后触发该状态机上报
this.isPlaying = true;
console.info(TAG, ‘AVPlayer state playing called.’);
break;
case ‘paused’: // pause成功调用后触发该状态机上报
console.info(TAG, ‘AVPlayer state paused called.’);
break;
case ‘stopped’: // stop接口成功调用后触发该状态机上报
console.info(TAG, ‘AVPlayer state stopped called.’);
break;
case ‘released’:
console.info(TAG, ‘AVPlayer state released called.’);
break;
case ‘error’:
console.info(TAG, ‘AVPlayer state error called’);
break;
default:
console.info(TAG, ‘AVPlayer state unknown called.’);
break;
}
})
}
setSpeed(speed: number) {
try {
if (!this.avPlayer) {
console.error(TAG, ‘setSpeed failed. avPlayer is undefined.’)
return;
}
let state = this.avPlayer.state;
if (state != StateEnum.prepared && state != StateEnum.playing && state != StateEnum.paused && state != StateEnum.completed) {
console.error(TAG, ‘setSpeed failed. state is not prepared/paused/completed.’)
return;
}
this.avPlayer.setSpeed(speed);
} catch (err) {
console.error(TAG, setSpeed failed. err is ${<span class="hljs-built_in"><span class="hljs-built_in">JSON</span></span>.stringify(err)}
)
}
}
setSeek(seek: number) {
try {
if (!this.avPlayer) {
console.error(TAG, ‘setSpeed failed. avPlayer is undefined.’)
return;
}
let state = this.avPlayer.state;
if (state != StateEnum.prepared && state != StateEnum.playing && state != StateEnum.paused && state != StateEnum.completed) {
console.error(TAG, ‘setSpeed failed. state is not prepared/playing/paused/completed.’)
return;
}
this.avPlayer.seek(seek, media.SeekMode.SEEK_PREV_SYNC);
} catch (err) {
console.error(TAG, setSpeed failed. err is ${<span class="hljs-built_in"><span class="hljs-built_in">JSON</span></span>.stringify(err)}
)
}
}
async play() {
try {
if (!this.avPlayer) {
console.error(TAG, ‘play failed. avPlayer is undefined.’)
return;
}
let state = this.avPlayer.state;
if (state != StateEnum.prepared && state != StateEnum.paused && state != StateEnum.completed) {
console.error(TAG, ‘play failed. state is not prepared/paused/completed.’)
return;
}
await this.avPlayer.play();
console.info(TAG, AVPlayer play successful;
);
} catch (err) {
console.error(TAG, AVPlayer play failed. err is ${<span class="hljs-built_in"><span class="hljs-built_in">JSON</span></span>.stringify(err)}
)
}
}
async pause() {
try {
if (!this.avPlayer) {
console.error(TAG, ‘pause failed. avPlayer is undefined.’)
return;
}
await this.avPlayer.pause();
console.info(TAG, AVPlayer pause successful;
);
} catch (err) {
console.error(TAG, AVPlayer pause failed. err is ${<span class="hljs-built_in"><span class="hljs-built_in">JSON</span></span>.stringify(err)}
)
}
}
async stop() {
try {
if (!this.avPlayer) {
console.error(TAG, ‘stop failed. avPlayer is undefined.’)
return;
}
let state = this.avPlayer.state;
if (state != StateEnum.prepared && state != StateEnum.playing && state != StateEnum.paused && state != StateEnum.completed) {
console.error(TAG, ‘setSpeed failed. state is not prepared/playing/paused/completed.’)
return;
}
await this.avPlayer.stop();
console.info(TAG, AVPlayer stop successful;
);
} catch (err) {
console.error(TAG, AVPlayer stop failed. err is ${<span class="hljs-built_in"><span class="hljs-built_in">JSON</span></span>.stringify(err)}
)
}
}
aboutToDisappear(): void {
console.info(TAG, enter aboutToDisappear. <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.curIndex:${<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.curIndex} <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.index:${<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.index}
)
if (this.avPlayer) {
this.avPlayer.off(‘timeUpdate’);
this.avPlayer.off(‘seekDone’);
this.avPlayer.off(‘speedDone’);
this.avPlayer.off(‘error’);
this.avPlayer.off(‘stateChange’);
this.avPlayer.release();
}
}
}
//MyDataSource.ets
// LazyForEach model.
import { media } from ‘@kit.MediaKit’;
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = [];
public totalCount(): number {
return 0;
}
public getData(index: number): media.AVFileDescriptor | string | undefined {
return undefined;
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded();
})
}
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
})
}
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index);
})
}
}
export class MyDataSource1 extends BasicDataSource {
public dataArray: media.AVFileDescriptor[] = [];
constructor(ele: Object[]) {
super();
for (let index = 0; index < ele.length; index++) {
this.dataArray.push(ele[index] as media.AVFileDescriptor);
}
}
public totalCount(): number {
return this.dataArray.length;
}
public getData(index: number): media.AVFileDescriptor {
return this.dataArray[index];
}
public addData(index: number, data: media.AVFileDescriptor): void {
this.dataArray.splice(index, 0);
this.notifyDataAdd(index);
}
}
export class MyDataSource extends BasicDataSource {
public dataArray: string[] = [];
constructor(ele: Object[]) {
super();
for (let index = 0; index < ele.length; index++) {
this.dataArray.push(ele[index] as string);
}
}
public totalCount(): number {
return this.dataArray.length;
}
public getData(index: number): string {
return this.dataArray[index];
}
public addData(index: number, data: media.AVFileDescriptor): void {
this.dataArray.splice(index, 0);
this.notifyDataAdd(index);
}
}
关于HarmonyOS 鸿蒙Next类似抖音首页视频播放效果的demo,以下是一个简要示例说明:
-
界面布局:
- 使用Tabbar布局,中间设置发布按钮,其余按钮使用文字。
- 自定义导航栏,覆盖在视频之上,包含两边的图片按钮和中间可滑动的菜单列表。
-
视频播放:
- 使用鸿蒙系统提供的Video组件播放视频。
- 设置视频的源地址、控制器等属性,并配置自动播放、隐藏控制器、适应模式等选项。
-
视频翻页:
- 使用Swiper组件实现视频翻页效果。
- 配置动画曲线、循环模式等属性,确保翻页流畅自然。
-
其他功能:
- 考虑实现画中画播放、后台播放等功能,提升用户体验。
- 优化折叠屏设备的适配,如全屏播放时将手机半折叠放在桌上,上半屏继续看剧,下半屏进行其他操作。
由于篇幅限制,无法提供完整的代码实现。但你可以基于上述说明,结合鸿蒙系统的开发文档和API进行具体实现。
HarmonyOS鸿蒙Next高级实战已发布,可以先学学:https://www.itying.com/goods-1204.html