<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 30075字,需瀏覽 61分鐘

           ·

          2024-04-10 14:36

          d0fb35e7961b559d63b37d3958a1558c.webp

          前段時(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)加載;

          效果展示

          a424f5dbe89b9849cdd904a92faa4a28.webp

          還是先上紅包封面吧

          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 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請(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';
                
                
                  
                    
          @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計(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].y
                
                
                        break;
                
                
                      case TouchType.Up: // 手指按下
                
                
                        this.offsetY = 0
                
                
                        this.isDone = false
                
                
                        break;
                
                
                      case TouchType.Move: // 手指移動(dòng)
                
                
                        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實(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> }, }
          @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({ //要計(jì)算的文本內(nèi)容,必填 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坐標(biāo) this.lastMoveY = event.touches[0].y break; case TouchType.Up: // 手指按下 this.offsetY = 0 this.isDone = false break; case TouchType.Move: // 手指移動(dòng) 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 || '標(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; }) } }


          瀏覽 31
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          自學(xué)HarmonyOS應(yīng)用開發(fā)(74)- 拍攝照片
          自學(xué)HarmonyOS應(yīng)用開發(fā)(49)- 引入地圖功能
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  波多野结衣无码AⅤ一区t二区三区 | 日韩黄色电影在线免费观看 | 久久免费小视频 | 亚洲三级无码视频 | 天天做天天爱天天综合网 |