【HarmonyOS开发】案例-短视频应用

共 30075字,需浏览 61分钟

 ·

2024-04-10 14:36


d0fb35e7961b559d63b37d3958a1558c.webp



前段时间看到一篇文章,但是没有源码,是一个仿写抖音的文章,最近也在看这块,顺便写个简单的短视频小应用。



技术点拆分


1、http请求数据;


2、measure计算文本宽度


3、video播放视频;


4、onTouch上滑/下拉切换视频;


5、List实现滚动加载;


效果展示


a424f5dbe89b9849cdd904a92faa4a28.webp


还是先上红包封面吧


http请求数据


通过对@ohos.net.http进行二次封装,进行数据请求。


1、封装requestHttp;


      
import http from '@ohos.net.http';








// 1、创建RequestOption.ets 配置类



export interface RequestOptions {


url?: string;


method?: RequestMethod; // default is GET


queryParams ?: Record<string, string>;


extraData?: string | Object | ArrayBuffer;


header?: Object; // default is 'content-type': 'application/json'


}







export enum RequestMethod {


OPTIONS = "OPTIONS",


GET = "GET",


HEAD = "HEAD",


POST = "POST",


PUT = "PUT",


DELETE = "DELETE",


TRACE = "TRACE",


CONNECT = "CONNECT"


}








/**




* Http请求器




*/



export class HttpCore {


/**



* 发送请求




* @param requestOption




* @returns Promise




*/



request<T>(requestOption: RequestOptions): Promise<T> {


return new Promise<T>((resolve, reject) => {


this.sendRequest(requestOption)


.then((response) => {


if (typeof response.result !== 'string') {


reject(new Error('Invalid data type'));







} else {


let bean: T = JSON.parse(response.result);


if (bean) {


resolve(bean);


} else {


reject(new Error('Invalid data type,JSON to T failed'));


}







}


})


.catch((error) => {


reject(error);


});


});


}







private sendRequest(requestOption: RequestOptions): Promise<http.HttpResponse> {


// 每一个httpRequest对应一个HTTP请求任务,不可复用


let httpRequest = http.createHttp();







let resolveFunction, rejectFunction;


const resultPromise = new Promise<http.HttpResponse>((resolve, reject) => {


resolveFunction = resolve;


rejectFunction = reject;


});







if (!this.isValidUrl(requestOption.url)) {


return Promise.reject(new Error('url格式不合法.'));


}







let promise = httpRequest.request(this.appendQueryParams(requestOption.url, requestOption.queryParams), {


method: requestOption.method,


header: requestOption.header,


extraData: requestOption.extraData, // 当使用POST请求时此字段用于传递内容


expectDataType: http.HttpDataType.STRING // 可选,指定返回数据的类型


});







promise.then((response) => {


console.info('Result:' + response.result);


console.info('code:' + response.responseCode);


console.info('header:' + JSON.stringify(response.header));







if (http.ResponseCode.OK !== response.responseCode) {


throw new Error('http responseCode !=200');


}


resolveFunction(response);







}).catch((err) => {


rejectFunction(err);


}).finally(() => {


// 当该请求使用完毕时,调用destroy方法主动销毁。


httpRequest.destroy();


})


return resultPromise;


}












private appendQueryParams(url: string, queryParams: Record<string, string>): string {


// todo 使用将参数拼接到url


return url;


}







private isValidUrl(url: string): boolean {


//todo 实现URL格式判断


return true;


}


}







// 实例化请求器


const httpCore = new HttpCore();












export class HttpManager {


private static mInstance: HttpManager;







// 防止实例化


private constructor() {


}







static getInstance(): HttpManager {


if (!HttpManager.mInstance) {


HttpManager.mInstance = new HttpManager();


}


return HttpManager.mInstance;


}












request<T>(option: RequestOptions): Promise<T> {


return new Promise(async (resolve, reject) => {


try {


const data: any = await httpCore.request(option)


resolve(data)


} catch (err) {


reject(err)


}


})


}


}







export default HttpManager;


2、使用request Http请求视频接口;


      
import httpManager, { RequestMethod } from '../../utils/requestHttp';







@State total: number = 0


@State listData: Array<ResultType> = []


private url: string = "https://api.apiopen.top/api/getHaoKanVideo?size=10";


private page: number = 0







private httpRequest() {


httpManager.getInstance()


.request({


method: RequestMethod.GET,


url: `${this.url}&page=${this.page}` //公开的API


})


.then((res: resultBean) => {


this.listData = [...this.listData, ...res.result.list];


this.total = res.result.total;


this.duration = 0;


this.rotateAngle = 0;


})


.catch((err) => {


console.error(JSON.stringify(err));


});


}


measure计算文本宽度


      
import measure from '@ohos.measure'







@State textWidth : number = measure.measureText({


//要计算的文本内容,必填


textContent: this.title,


})




// this.textWidth可以获取this.title的宽度



video播放视频


1、通过videoController控制视频的播放和暂停,当一个视频播放结束,播放下一个


      
private videoController: VideoController = new VideoController()







Video({


src: this.playUrl,


previewUri: this.coverUrl,


controller: this.videoController


})


.width('100%')


.height('100%')


.borderRadius(3)


.controls(false)


.autoPlay(true)


.offset({ x: 0, y: `${this.offsetY}px` })


.onFinish(() => {


this.playNext()


})


2、Video的一些常用方法


属性:





































名称 参数类型 描述
muted boolean 是否静音。
默认值:false
autoPlay boolean 是否自动播放。
默认值:false
controls boolean 控制视频播放的控制栏是否显示。
默认值:true
objectFit ImageFit 设置视频显示模式。
默认值:Cover
loop boolean 是否单个视频循环播放。
默认值:false

事件:















































名称 功能描述
onStart(event:() => void) 播放时触发该事件。
onPause(event:() => void) 暂停时触发该事件。
onFinish(event:() => void) 播放结束时触发该事件。
onError(event:() => void) 播放失败时触发该事件。
onPrepared(callback:(event: { duration: number }) => void) 视频准备完成时触发该事件。
duration:当前视频的时长,单位为秒(s)。
onSeeking(callback:(event: { time: number }) => void) 操作进度条过程时上报时间信息。
time:当前视频播放的进度,单位为s。
onSeeked(callback:(event: { time: number }) => void) 操作进度条完成后,上报播放时间信息。
time:当前视频播放的进度,单位为s。
onUpdate(callback:(event: { time: number }) => void) 播放进度变化时触发该事件。
time:当前视频播放的进度,单位为s。
onFullscreenChange(callback:(event: { fullscreen: boolean }) => void) 在全屏播放与非全屏播放状态之间切换时触发该事件。
fullscreen:返回值为true表示进入全屏播放状态,为false则表示非全屏播放。

onTouch上滑/下拉切换视频


通过手指按压时,记录Y的坐标,移动过程中,如果移动大于50,则进行上一个视频或者下一个视频的播放。


      
private onTouch = ((event) => {


switch (event.type) {


case TouchType.Down: // 手指按下


// 记录按下的y坐标


this.lastMoveY = event.touches[0].y


break;


case TouchType.Up: // 手指按下


this.offsetY = 0


this.isDone = false


break;


case TouchType.Move: // 手指移动


const offsetY = (event.touches[0].y - this.lastMoveY) * 3;


let isDownPull = offsetY < -80


let isUpPull = offsetY > 80


this.lastMoveY = event.touches[0].y


if(isUpPull || isDownPull) {


this.offsetY = offsetY


this.isDone = true


}







console.log('=====offsetY======', this.offsetY, isDownPull, isUpPull)







if (isDownPull && this.isDone) {


this.playNext()


}


if (isUpPull && this.isDone) {


this.playNext()


}


break;


}


})


List实现滚动加载


1、由于视频加载会比较慢,因此List中仅展示一个视频的图片,点击播放按钮即可播放;


2、通过onScrollIndex监听滚动事件,如果当前数据和滚动的index小于3,则进行数据下一页的请求;


      
List({ scroller: this.scroller, space: 12 }) {


ForEach(this.listData, (item: ResultType, index: number) => {


ListItem() {


Stack({ alignContent: Alignment.TopStart }) {


Row() {


Image(item.userPic).width(46).height(46).borderRadius(12).margin({ right: 12 }).padding(6)


Text(item.title || '标题').fontColor(Color.White).width('80%')


}


.width('100%')


.backgroundColor('#000000')


.opacity(0.6)


.alignItems(VerticalAlign.Center)


.zIndex(9)







Image(item.coverUrl)


.width('100%')


.height(320)


.alt(this.imageDefault)







Row() {


Image($rawfile('play.png')).width(60).height(60)


}


.width('100%')


.height('100%')


.justifyContent(FlexAlign.Center)


.alignItems(VerticalAlign.Center)


.opacity(0.8)


.zIndex(100)


.onClick(() => {


this.currentPlayIndex = index;


this.coverUrl = item.coverUrl;


this.playUrl = item.playUrl;


this.videoController.start()


})


}


.width('100%')


.height(320)


}


  })


}


.divider({ strokeWidth: 1, color: 'rgb(247,247,247)', startMargin: 60, endMargin: 0 })


.onScrollIndex((start, end) => {


console.log('============>', start, end)


if(this.listData.length - end < 3) {


this.page = this.page++


this.httpRequest()


}


})


完整代码


      
import httpManager, { RequestMethod } from '../../utils/requestHttp';


import measure from '@ohos.measure'


import router from '@ohos.router';







type ResultType = {


id: number;


title: string;


userName: string;


userPic: string;


coverUrl: string;


playUrl: string;


duration: string;


}







interface resultBean {


code: number,


message: string,


result: {


total: number,


list: Array<ResultType>


},


}








@Entry




@Component



export struct VideoPlay {


scroller: Scroller = new Scroller()


private videoController: VideoController = new VideoController()


@State total: number = 0


@State listData: Array<ResultType> = []


private url: string = "https://api.apiopen.top/api/getHaoKanVideo?size=10";


private page: number = 0







private httpRequest() {


httpManager.getInstance()


.request({


method: RequestMethod.GET,


url: `${this.url}&page=${this.page}` //公开的API


})


.then((res: resultBean) => {


this.listData = [...this.listData, ...res.result.list];


this.total = res.result.total;


this.duration = 0;


this.rotateAngle = 0;


})


.catch((err) => {


console.error(JSON.stringify(err));


});


}







aboutToAppear() {


this.httpRequest()


}







@State currentPlayIndex: number = 0


@State playUrl: string = ''


@State coverUrl: string = ''


@State imageDefault: any = $rawfile('noData.svg')







@State offsetY: number = 0


private lastMoveY: number = 0







playNext() {


const currentItem = this.listData[this.currentPlayIndex + 1]


this.currentPlayIndex = this.currentPlayIndex + 1;


this.coverUrl = currentItem?.coverUrl;


this.playUrl = currentItem?.playUrl;


this.videoController.start()


this.scroller.scrollToIndex(this.currentPlayIndex - 1)







if(this.listData.length - this.currentPlayIndex < 3) {


this.page = this.page++


this.httpRequest()


}


}







playPre() {


const currentItem = this.listData[this.currentPlayIndex - 1]


this.currentPlayIndex = this.currentPlayIndex +- 1;


this.coverUrl = currentItem?.coverUrl;


this.playUrl = currentItem?.playUrl;


this.videoController.start()


this.scroller.scrollToIndex(this.currentPlayIndex - 2)


}







private title: string = 'Harmony短视频';


@State screnWidth: number = 0;


@State screnHeight: number = 0;


@State textWidth : number = measure.measureText({


//要计算的文本内容,必填


textContent: this.title,


})


@State rotateAngle: number = 0;


@State duration: number = 0;







private isDone: boolean = false







@State isPlay: boolean = true







build() {


Stack({ alignContent: Alignment.TopEnd }) {


Row() {


Stack({ alignContent: Alignment.TopStart }) {


Button() {


Image($r('app.media.ic_public_arrow_left')).width(28).height(28).margin({ left: 6, top: 3, bottom: 3 })


}.margin({ left: 12 }).backgroundColor(Color.Transparent)


.onClick(() => {


router.back()


})


Text(this.title).fontColor(Color.White).fontSize(18).margin({ top: 6 }).padding({ left: (this.screnWidth - this.textWidth / 3) / 2 })







Image($r('app.media.ic_public_refresh')).width(18).height(18)


.margin({ left: this.screnWidth - 42, top: 8 })


.rotate({ angle: this.rotateAngle })


.animation({


duration: this.duration,


curve: Curve.EaseOut,


iterations: 1,


playMode: PlayMode.Normal


})


.onClick(() => {


this.duration = 1200;


this.rotateAngle = 360;


this.page = 0;


this.listData = [];


this.httpRequest();


})


}


}


.width('100%')


.height(60)


.backgroundColor(Color.Black)


.alignItems(VerticalAlign.Center)







if(this.playUrl) {


Column() {


Text('')


}


.backgroundColor(Color.Black)


.zIndex(997)


.width('100%')


.height('100%')


if(!this.isPlay) {


Image($r('app.media.pause')).width(46).height(46)


.margin({


right: (this.screnWidth - 32) / 2,


top: (this.screnHeight - 32) / 2


})


.zIndex(1000)


.onClick(() => {


this.isPlay = true


this.videoController.start()


})


}







Image($rawfile('close.png')).width(32).height(32).margin({


top: 24,


right: 24


})


.zIndex(999)


.onClick(() => {


this.videoController.stop()


this.playUrl = ''


})


Video({


src: this.playUrl,


previewUri: this.coverUrl,


controller: this.videoController


})


.zIndex(998)


.width('100%')


.height('100%')


.borderRadius(3)


.controls(false)


.autoPlay(true)


.offset({ x: 0, y: `${this.offsetY}px` })


.onFinish(() => {


this.playNext()


})


.onClick(() => {


this.isPlay = false


this.videoController.stop()


})


.onTouch((event) => {


switch (event.type) {


case TouchType.Down: // 手指按下


// 记录按下的y坐标


this.lastMoveY = event.touches[0].y


break;


case TouchType.Up: // 手指按下


this.offsetY = 0


this.isDone = false


break;


case TouchType.Move: // 手指移动


const offsetY = (event.touches[0].y - this.lastMoveY) * 3;


let isDownPull = offsetY < -80


let isUpPull = offsetY > 80


this.lastMoveY = event.touches[0].y


if(isUpPull || isDownPull) {


this.offsetY = offsetY


this.isDone = true


}







console.log('=====offsetY======', this.offsetY, isDownPull, isUpPull)







if (isDownPull && this.isDone) {


this.playNext()


}


if (isUpPull && this.isDone) {


this.playNext()


}


break;


}


})


}


List({ scroller: this.scroller, space: 12 }) {


ForEach(this.listData, (item: ResultType, index: number) => {


ListItem() {


Stack({ alignContent: Alignment.TopStart }) {


Row() {


Image(item.userPic).width(46).height(46).borderRadius(12).margin({ right: 12 }).padding(6)


Text(item.title || '标题').fontColor(Color.White).width('80%')


}


.width('100%')


.backgroundColor('#000000')


.opacity(0.6)


.alignItems(VerticalAlign.Center)


.zIndex(9)







Image(item.coverUrl)


.width('100%')


.height(320)


.alt(this.imageDefault)







Row() {


Image($rawfile('play.png')).width(60).height(60)


}


.width('100%')


.height('100%')


.justifyContent(FlexAlign.Center)


.alignItems(VerticalAlign.Center)


.opacity(0.8)


.zIndex(100)


.onClick(() => {


this.currentPlayIndex = index;


this.coverUrl = item.coverUrl;


this.playUrl = item.playUrl;


this.videoController.start()


})


}


.width('100%')


.height(320)


}


.padding({


left: 6,


right: 6,


bottom: 6


})


})


}


.width('100%')


.margin(6)


.position({ y: 66 })


.divider({ strokeWidth: 1, color: 'rgb(247,247,247)', startMargin: 60, endMargin: 0 })


.onScrollIndex((start, end) => {


console.log('============>', start, end)


if(this.listData.length - end < 3) {


this.page = this.page++


this.httpRequest()


}


})


}


.onAreaChange((_oldValue: Area, newValue: Area) => {


this.screnWidth = newValue.width as number;


this.screnHeight = newValue.height as number;


})


}


}




浏览 31
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报