前言
之前在web中实现过该功能,想着直接搬过来修改一下,也能在OpenHarmony上跑起来。其实360度全景展示功能的用途还是挺多的,比如一些购物平台用于全面展示一件商品,这样可以更全面直观的了解这件商品;还有一些售楼平台,可以去展示一些全景户型等等。
项目说明
- 工具版本:DevEco Studio 3.0 Beta2。
- SDK版本:3.0.5.2(API Version 7 Beta2)。
- 主要组件:canvas。
效果展示
实现原理
基于canvas画布,通过绘制一个360°的序列帧图片实现的。 通过监听手势滑动来实现图片帧切换。
实现过程
一、 创建canvas画布
因为是基于canvas实现的,首先当然要写个canvas画布标签,并添加两个手势事件touchstart和touchmove。
<div class="container">
<div class="tt" id="tt"></div>
<canvas
id="canvas"
ref="canvas"
@touchstart="touchStart"
@touchmove="touchMove"
></canvas>
</div>
二、 加载所有序列帧图片
因为是通过图片序列帧实现的,所以需要预加载完成所有序列帧图片才能进行后续操作,否则会出现在切换图片的时候还没加载出来导致空白问题。
下面封装了一个imgLoad方法来处理所有图片预加载。
/**
* @param {Array} imgList 需要预加载图片的数组
* @param {Function} progressCb 加载进度回调方法
* @param {Function} completeCb 全部加载完成回调
*/
const imgLoad = (imgList = [], progressCb, completeCb) => {
let len = imgList.length;
let num = 0;
let progress = 0;
var loadImage = function (src) {
return new Promise(function (resolve, reject) {
let img = new Image();
img.onload = function () {
resolve(img); //加载时执行resolve函数
};
img.onerror = function () {
reject('地址错误:' + src); //抛出异常时执行reject函数
};
img.src = src;
});
};
function* fn() {
for (let i = 0; i < len; i++) {
yield loadImage(imgList[i]);
}
}
let g = fn();
let value = g.next().value;
resume();
function resume() {
console.log('====')
value.then((img) => {
// 单张加载完成
num++;
progress = parseFloat((100 / len) * num).toFixed(2);
progressCb && progressCb(img, progress);
value = g.next().value;
if (value) {
resume();
} else {
// 全部加载完成
completeCb && completeCb();
}
}).catch((err)=>{
console.log(err)
});
}
}
上面方法有两个回调。
- progressCb:单张加载完成回调,该回调可以监听到加载的进度和获取到创建的image对象。
- completeCb: 全部加载完成后回调,该回调成功后可以去做后续的操作了。
这里有一个比较少见的函数,fn* :生成器函数(generator function)。
什么是生成器函数呢?
生成器函数在执行时能够暂停,后面又能从暂停后继续执行。 使用yield关键字可以暂停函数。
调用一个生成器函数,会得到生成器的迭代器对象。
使用next()方法。被首次(后续)调用时,其内的语句会执行到第一个(后续)出现yield的位置为止,yield 后紧跟迭代器要返回的值。
yield*(多了个星号),则表示将执行权移交给另一个生成器函数(当前生成器暂停执行)。
next()-> {value:value1,done:true|false}。
- value:表示本次返回的值,即yield表达式返回的值
- done:表示生成器后续是否还有yield语句,即生成器函数是否已经执行完毕并返回。
注意:next()方法时,如果传入了参数,那么这个参数会作为上一条直线的yield语句的返回值
三、调用图片预加载方法
- 初始化canvas画布。
- 创建序列帧图片路径数组。
- 调用imgLoad方法。
initCanvas(){
let c = this.$refs.canvas;
this.ctx = c.getContext('2d');
let arr = [];
for(var i = 1; i <= this.imgLen; i++){
arr.push(`common/images/car/${i}.png`)
}
imgLoad(arr,(img, progress) => {
lg.log('进度:' + progress);
this.imgList.push(img);
},() => {
lg.log('全部加载完成')
this.cutSpirit();
})
},
四、 监听手势
touchStart方法监听触摸开始时手势,缓存当前触摸位置的x轴数据作为开始数据。
touchMove方法是监听滑动中的手势监听。
- 获取当前滑动的x轴位置,和缓存的startPoint进行比较。
- unit 是滑动精度单位,当前处理为屏幕宽度除于2倍的序列帧长度360/(64 * 2),就是从x轴为0到屏幕最右边可以完成2次序列帧旋转。分子越大,滑动越快。
- 每次滑动都需要把当前的位置缓存为startPoint。
- 从屏幕来看,右边滑动到左边,x值是减小的,所以定义type为right。
- 从屏幕来看,左边滑动到右边,x值是增大的,所以定义type为left。
- 注意:这里定义的type并不是序列帧需要走的type,下一点做详细讲解。
touchStart(e){
let s = e.touches[0].localX;
this.startPoint = s;
// lg.log('触摸开始:',s);
},
touchMove(e){
let s = e.touches[0].localX;
if((s - this.startPoint) > this.unit){
this.drawImg(this.imgIndex, 'right')
this.startPoint = s;
lg.log('向右:',s)
} else if((s - this.startPoint) < -this.unit){
this.drawImg(this.imgIndex, 'left')
this.startPoint = s;
lg.log('向左:',s)
}
this.startPoint = s;
},
五、绘制序列帧图片
- 手指滑动的方向不一定是序列帧图片滑动的方向,还需要看序列帧渲染的顺序是顺时针还是逆时针。
- 如果序列帧是顺时针,type为right是跟序列帧反方向,所以需要逆时针绘制序列帧,所以this.imgIndex–;。
- 如果序列帧是逆时针,type为left是跟序列帧同方向,所以顺时针绘制序列帧,所以this.imgIndex++,;。
/**
* @param {Number} n 当前绘制的序列帧下标
* @param {String} type 当前手指滑动的方向
*/
drawImg(n,type){
if(type == "right"){
if(this.imgIndex > 0){
this.imgIndex--;
}else{
this.imgIndex = this.imgLen;
}
}else if(type == "left"){
if(this.imgIndex < this.imgLen){
this.imgIndex++;
}else{
this.imgIndex = this.startIndex;
}
}
this.ctx.clearRect(0,0,this.w,this.h);
this.ctx.drawImage(this.imgList[this.imgIndex],0,0,this.w,this.h);
},
最终效果
代码地址
https://gitee.com/yango520/ohos-panoramic。
总结
到这里就基本完成,主要的技术点在于预加载所有序列帧图片和监听手势来绘制序列帧的。