前段時(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 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、使用requestHttp請(qǐng)求視頻接口;
import httpManager, { RequestMethod } from '../../utils/requestHttp';
measure計(jì)算文本寬度
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));
});
}
video播放視頻
1、通過(guò)videoController控制視頻的播放和暫停,當(dāng)一個(gè)視頻播放結(jié)束,播放下一個(gè)
import measure from '@ohos.measure'
@State textWidth : number = measure.measureText({
//要計(jì)算的文本內(nèi)容,必填
textContent: this.title,
})
// this.textWidth可以獲取this.title的寬度
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;
})
}
}
為了能讓大家更好的學(xué)習(xí)鴻蒙 (OpenHarmony) 開發(fā)技術(shù),這邊特意整理了《鴻蒙 (OpenHarmony)開發(fā)學(xué)習(xí)手冊(cè)》(共計(jì)890頁(yè)),希望對(duì)大家有所幫助:https://qr21.cn/FV7h05
《鴻蒙 (OpenHarmony)開發(fā)學(xué)習(xí)手冊(cè)》
入門必看:https://qr21.cn/FV7h05
- 應(yīng)用開發(fā)導(dǎo)讀(ArkTS)
- ……
HarmonyOS 概念:https://qr21.cn/FV7h05
- 系統(tǒng)定義
- 技術(shù)架構(gòu)
- 技術(shù)特性
- 系統(tǒng)安全
如何快速入門?:https://qr21.cn/FV7h05
- 基本概念
- 構(gòu)建第一個(gè)ArkTS應(yīng)用
- 構(gòu)建第一個(gè)JS應(yīng)用
- ……
開發(fā)基礎(chǔ)知識(shí):https://qr21.cn/FV7h05
- 應(yīng)用基礎(chǔ)知識(shí)
- 配置文件
- 應(yīng)用數(shù)據(jù)管理
- 應(yīng)用安全管理
- 應(yīng)用隱私保護(hù)
- 三方應(yīng)用調(diào)用管控機(jī)制
- 資源分類與訪問(wèn)
- 學(xué)習(xí)ArkTS語(yǔ)言
- ……
基于ArkTS 開發(fā):https://qr21.cn/FV7h05
1.Ability開發(fā)
2.UI開發(fā)
3.公共事件與通知
4.窗口管理
5.媒體
6.安全
7.網(wǎng)絡(luò)與鏈接
8.電話服務(wù)
9.數(shù)據(jù)管理
10.后臺(tái)任務(wù)(Background Task)管理
11.設(shè)備管理
12.設(shè)備使用信息統(tǒng)計(jì)
13.DFX
14.國(guó)際化開發(fā)
15.折疊屏系列
16.……文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-836257.html
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-836257.html
到了這里,關(guān)于HarmonyOS 實(shí)戰(zhàn)開發(fā)案例-仿抖音短視頻應(yīng)用的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!