【HarmonyOS開發(fā)】案例-短視頻應(yīng)用

前段時(shí)間看到一篇文章,但是沒(méi)有源碼,是一個(gè)仿寫抖音的文章,最近也在看這塊,順便寫個(gè)簡(jiǎn)單的短視頻小應(yīng)用。
技術(shù)點(diǎn)拆分
1、http請(qǐng)求數(shù)據(jù);
2、measure計(jì)算文本寬度 ;
3、video播放視頻;
4、onTouch上滑/下拉切換視頻;
5、List實(shí)現(xiàn)滾動(dòng)加載;
效果展示

還是先上紅包封面吧
http請(qǐng)求數(shù)據(jù)
通過(guò)對(duì)@ohos.net.http進(jìn)行二次封裝,進(jìn)行數(shù)據(jù)請(qǐng)求。
1、封裝requestHttp;
import http from '@ohos.net.http';
// 1、創(chuàng)建RequestOption.ets 配置類export interface RequestOptions {url?: string;method?: RequestMethod; // default is GETqueryParams ?: 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請(qǐng)求器*/export class HttpCore {/*** 發(fā)送請(qǐng)求* @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> {// 每一個(gè)httpRequest對(duì)應(yīng)一個(gè)HTTP請(qǐng)求任務(wù),不可復(fù)用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, // 當(dāng)使用POST請(qǐng)求時(shí)此字段用于傳遞內(nèi)容expectDataType: http.HttpDataType.STRING // 可選,指定返回?cái)?shù)據(jù)的類型});
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(() => {// 當(dāng)該請(qǐng)求使用完畢時(shí),調(diào)用destroy方法主動(dòng)銷毀。httpRequest.destroy();})return resultPromise;}
private appendQueryParams(url: string, queryParams: Record<string, string>): string {// todo 使用將參數(shù)拼接到url上return url;}
private isValidUrl(url: string): boolean {//todo 實(shí)現(xiàn)URL格式判斷return true;}}
// 實(shí)例化請(qǐng)求器const httpCore = new HttpCore();
export class HttpManager {private static mInstance: HttpManager;
// 防止實(shí)例化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請(qǐng)求視頻接口;
import httpManager, { RequestMethod } from '../../utils/requestHttp';
total: number = 0listData: 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計(jì)算文本寬度
import measure from '@ohos.measure'
@State textWidth : number = measure.measureText({//要計(jì)算的文本內(nèi)容,必填textContent: this.title,})// this.textWidth可以獲取this.title的寬度
video播放視頻
1、通過(guò)videoController控制視頻的播放和暫停,當(dāng)一個(gè)視頻播放結(jié)束,播放下一個(gè)
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的一些常用方法
屬性:
| 名稱 | 參數(shù)類型 | 描述 |
|---|---|---|
| muted | boolean | 是否靜音。 默認(rèn)值:false |
| autoPlay | boolean | 是否自動(dòng)播放。 默認(rèn)值:false |
| controls | boolean | 控制視頻播放的控制欄是否顯示。 默認(rèn)值:true |
| objectFit | ImageFit | 設(shè)置視頻顯示模式。 默認(rèn)值:Cover |
| loop | boolean | 是否單個(gè)視頻循環(huán)播放。 默認(rèn)值:false |
事件:
| 名稱 | 功能描述 |
|---|---|
| onStart(event:() => void) | 播放時(shí)觸發(fā)該事件。 |
| onPause(event:() => void) | 暫停時(shí)觸發(fā)該事件。 |
| onFinish(event:() => void) | 播放結(jié)束時(shí)觸發(fā)該事件。 |
| onError(event:() => void) | 播放失敗時(shí)觸發(fā)該事件。 |
| onPrepared(callback:(event: { duration: number }) => void) | 視頻準(zhǔn)備完成時(shí)觸發(fā)該事件。 duration:當(dāng)前視頻的時(shí)長(zhǎng),單位為秒(s)。 |
| onSeeking(callback:(event: { time: number }) => void) | 操作進(jìn)度條過(guò)程時(shí)上報(bào)時(shí)間信息。 time:當(dāng)前視頻播放的進(jìn)度,單位為s。 |
| onSeeked(callback:(event: { time: number }) => void) | 操作進(jìn)度條完成后,上報(bào)播放時(shí)間信息。 time:當(dāng)前視頻播放的進(jìn)度,單位為s。 |
| onUpdate(callback:(event: { time: number }) => void) | 播放進(jìn)度變化時(shí)觸發(fā)該事件。 time:當(dāng)前視頻播放的進(jìn)度,單位為s。 |
| onFullscreenChange(callback:(event: { fullscreen: boolean }) => void) | 在全屏播放與非全屏播放狀態(tài)之間切換時(shí)觸發(fā)該事件。 fullscreen:返回值為true表示進(jìn)入全屏播放狀態(tài),為false則表示非全屏播放。 |
onTouch上滑/下拉切換視頻
通過(guò)手指按壓時(shí),記錄Y的坐標(biāo),移動(dòng)過(guò)程中,如果移動(dòng)大于50,則進(jìn)行上一個(gè)視頻或者下一個(gè)視頻的播放。
private onTouch = ((event) => {switch (event.type) {case TouchType.Down: // 手指按下// 記錄按下的y坐標(biāo)this.lastMoveY = event.touches[0].ybreak;case TouchType.Up: // 手指按下this.offsetY = 0this.isDone = falsebreak;case TouchType.Move: // 手指移動(dòng)const offsetY = (event.touches[0].y - this.lastMoveY) * 3;let isDownPull = offsetY < -80let isUpPull = offsetY > 80this.lastMoveY = event.touches[0].yif(isUpPull || isDownPull) {this.offsetY = offsetYthis.isDone = true}
console.log('=====offsetY======', this.offsetY, isDownPull, isUpPull)
if (isDownPull && this.isDone) {this.playNext()}if (isUpPull && this.isDone) {this.playNext()}break;}})
List實(shí)現(xiàn)滾動(dòng)加載
1、由于視頻加載會(huì)比較慢,因此List中僅展示一個(gè)視頻的圖片,點(diǎn)擊播放按鈕即可播放;
2、通過(guò)onScrollIndex監(jiān)聽滾動(dòng)事件,如果當(dāng)前數(shù)據(jù)和滾動(dòng)的index小于3,則進(jìn)行數(shù)據(jù)下一頁(yè)的請(qǐng)求;
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 || '標(biāo)題').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>},}
export struct VideoPlay {scroller: Scroller = new Scroller()private videoController: VideoController = new VideoController()total: number = 0listData: 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()}
currentPlayIndex: number = 0playUrl: string = ''coverUrl: string = ''imageDefault: any = $rawfile('noData.svg')
offsetY: number = 0private 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短視頻';screnWidth: number = 0;screnHeight: number = 0;textWidth : number = measure.measureText({//要計(jì)算的文本內(nèi)容,必填textContent: this.title,})rotateAngle: number = 0;duration: number = 0;
private isDone: boolean = false
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 = truethis.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 = falsethis.videoController.stop()}).onTouch((event) => {switch (event.type) {case TouchType.Down: // 手指按下// 記錄按下的y坐標(biāo)this.lastMoveY = event.touches[0].ybreak;case TouchType.Up: // 手指按下this.offsetY = 0this.isDone = falsebreak;case TouchType.Move: // 手指移動(dòng)const offsetY = (event.touches[0].y - this.lastMoveY) * 3;let isDownPull = offsetY < -80let isUpPull = offsetY > 80this.lastMoveY = event.touches[0].yif(isUpPull || isDownPull) {this.offsetY = offsetYthis.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 || '標(biāo)題').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;})}}
評(píng)論
圖片
表情
