之前需要在小程序里实现对一张图片的单指拖动双指缩放效果。试了网上很多别人的代码已经微信自己的一些控件,基本双指缩放的时候都是会以左上角为原点进行缩放,而微信自己的那些控件使用的时候又比较有局限性(具体放弃原因已经忘了),效果不太理想。后来索性自己写了个组件来实现。
单指移动很简单,主要就是通过控制
标签style
中的margin-top
和margin-left
来实现。
双指缩放的话,通过改变
标签的宽高来改变大小,为了保证缩放的中心点为双指中心点,我们同样通过动态的控制margin-top
和margin-left
来实现。
先看一下是不是想要的效果吧
height="498" width="320" src="http://ow2hkke1k.bkt.clouddn.com/42183227ac01ef5548c2f6685dc12eda_6559509642583883858.mp4">
如果不想看代码解释的话,可以直接拖到最下面直接下载使用。垃圾代码,没什么好解释的。
为了方便扩展和复用,我们使用组件的方式来实现这个需求,命名一个为zoomImgByView
的组件,组件的wxml代码如下
<view
style="width:{{view_width}}px;height:{{view_height}}px;background:red">
<image
id="mapImage"
style="width:{{imgWidth}}px;height:{{imgHeight}}px;position: relative;top:0px;bottom:0px;margin-top:{{marginTop}}px;margin-left:{{marginLeft}}px;"
src="{{img_src}}"
mode="aspectFill"
catchload="_imgLoadEvent"
catchtouchstart='_touchStartEvent'
catchtouchmove='_touchMoveEvent'
catchtouchend='_touchEndEvent'
catchtouchcancel='_touchEndEvent'/>
view>
基本就是一个view里面套了一个image。其中view_width
和view_height
为图片显示区域的大小,imgWidth
和imgHeight
为图片的实际宽高。这当中img_src
、view_width
、view_height
三个属性时需要开放给页面调用的,所以组件的properties
中我们写上这三个属性。
//图片地址
img_src: {
type: String
},
//可视区域的大小
view_width: {
type: String
},
view_height: {
type: String
}
再来看
标签中样式相关的属性,主要需要控制的四个属性是imgWidth
、imgHeight
、marginTop
、marginLeft
,我们在组件的data
中初始化这四个属性。
data: {
imgWidth:0,
imgHeight:0,
marginTop:0,
marginLeft:0
},
最后再在组件的JS代码最前面定义一些后面会用到的变量
var lastTouchPoint = { x: 0, y: 0 };//记录上次触摸时手指的点
var newDist = 0;//本次触摸事件双指间距
var oldDist = 0;//上次触摸事件双指间距
var inTouch = false; //是否处于触摸过程中,如果在触摸则不执行回弹动画
至此我们组件的一些相关变量定义以及完成,已经可以在页面中调用了,我们在main.json(我的页面文件名字叫main)中声明这个组件
{
"usingComponents": {
"zoomImgByView": "/component/zoomImgByView/zoomImgByView"
}
}
然后再在main.wxml中引用该组件
<zoomImgByView
img_src="{{imgSrc}}"
view_width="{{viewWidth}}"
view_height="{{viewHeight}}"/>
在main.js中初始化这些控制变量,我直接在onload事件中写的,并且将图片显示区域设置为屏幕大小。
onLoad: function (options) {
wx.getSystemInfo({
success: res => {
this.setData({
viewHeight: res.windowHeight,
viewWidth: res.windowWidth,
imgSrc:"http://bizhi.sogou.com/bizhi/images/newpark/bg1_1.jpg"
})
}
})
},
到此位置,组件的初始化工作已经完成了,不过我们是看不到图片的,因为现在的宽高还是0,加下来我们来具体实现组件中的一些方法。
前面我们在组件的wxml文件中写了 catchload="_imgLoadEvent"
,这个方法是image标签图片加载完成后调用的,具体实现代码如下:
lastTouchPoint = { x: 0, y: 0 };
var ratio;
var heightRatio = event.detail.height / this.data.view_height;
var widthRatio = event.detail.width / this.data.view_width;
ratio = widthRatio;
if (widthRatio > heightRatio) {
ratio = heightRatio
}
this.setData({
imgWidth: event.detail.width / ratio,
imgHeight: event.detail.height / ratio,
marginLeft: -(event.detail.width / ratio - this.data.view_width) / 2,
marginTop: -(event.detail.height / ratio - this.data.view_height) / 2,
})
//打开定时器一直计算是否需要执行回弹动画
setInterval(e => {
if (!inTouch) {
this._reboundAnimation();
}
}, 5)
基本逻辑是先获取到图片原本的宽高,然后根据图片原本宽高和组件显示区域宽高的比例,来确定图片改如何进行第一次缩放以铺满组件显示区域。通过ratio变量来确定图片第一次缩放的比例,计算出图片超出显示区域大小来确定marginLeft
和marginTop
的初始值。
接着我们打开了一个定时器,没5ms来执行一次_reboundAnimation()方法,此方法用来进行image标签的回弹效果,需要注意的是,此方法由于一直执行,消耗了大量的CPU,所以此处需要谨慎处理。
当触摸动作开始的时候,我们需要进行一些赋值操作,wxml中的catchtouchstart='_touchStartEvent'
为触摸开始调用的方法,具体实现如下:
/**
* 触摸开始事件
*/
_touchStartEvent: function () {
inTouch = true
lastTouchPoint = { x: 0, y: 0 }
oldDist = 0
},
分别是将触摸状态设置为true,防止在触摸事件中执行了回弹动画,将上次触摸点归零,上次的双指距离归为零。
当在触摸过程中的时候,开始对样式进行一些调整,catchtouchmove='_touchMoveEvent'
事件的具体实现分为两步,分别是单指拖动和双指缩放:
_touchMoveEvent
中单指移动事件代码如下
_touchMoveEvent: function (e) {
//单指移动事件
if (e.touches.length == 1) {
if (lastTouchPoint.x == 0 && lastTouchPoint.y == 0) {
lastTouchPoint.x = e.touches[0].clientX
lastTouchPoint.y = e.touches[0].clientY
} else {
var xOffset = e.touches[0].clientX - lastTouchPoint.x
var yOffset = e.touches[0].clientY - lastTouchPoint.y
this.setData({
marginTop: this.data.marginTop + yOffset,
marginLeft: this.data.marginLeft + xOffset,
})
lastTouchPoint.x = e.touches[0].clientX
lastTouchPoint.y = e.touches[0].clientY
}
console.log(this.data.marginTop)
}
如果是本次触摸事件的第一次执行,则将本次触摸点的值赋给lastTouchPoint
,否则计算出本次触摸点和上次的偏差后,修改marginTop
和marginLeft
的值后,再将本次触摸点的值赋给lastTouchPoint
,用作下次计算。单指拖动很简单,不赘述。
先在单指拖动的代码后面追加如下代码:
//双指缩放事件
if (e.touches.length == 2) {
if (oldDist == 0) {
oldDist = this._spacing(e);
} else {
newDist = this._spacing(e);
if (newDist > oldDist + 1) {
this._zoom(newDist / oldDist, e);
oldDist = newDist;
}
if (newDist < oldDist - 1) {
this._zoom(newDist / oldDist, e);
oldDist = newDist;
}
}
}
其中oldDist
为上次触摸事件时,两指的距离。显而易见,newDist
表示本次触摸事件两指距离。_spacing(e)
方法负责计算出两指之间的距离,_zoom(f,e)
方法负责处理图片的实际缩放,方法中的f是缩放的比例。
_spacing(e)
方法的具体实现如下
/**
* 计算两指间距
*/
_spacing: function (event) {
var x = event.touches[0].clientX - event.touches[1].clientX;
var y = event.touches[0].clientY - event.touches[1].clientY;
return Math.sqrt(x * x + y * y);
},
而_zoom(f,e)
方法的具体实现如下:
_zoom: function (f, event) {
var xRatio = this._calcXRatio(event)
var yRatio = this._calcYRatio(event)
if (this.data.imgWidth <= this.data.view_width && f < 1) {
var ratio = this.data.view_width / this.data.imgWidth
this.setData({
imgWidth: this.data.imgWidth * ratio,
imgHeight: this.data.imgHeight * ratio
})
return;
}
if (this.data.imgHeight <= this.data.view_height && f < 1) {
var ratio = this.data.view_height / this.data.imgHeight
this.setData({
imgWidth: this.data.imgWidth * ratio,
imgHeight: this.data.imgHeight * ratio
})
return;
}
this.setData({
//此处的ratio为双指中心点在图片的百分比
marginLeft: this.data.marginLeft + xRatio * this.data.imgWidth * (1 - f),
marginTop: this.data.marginTop + yRatio * this.data.imgHeight * (1 - f),
imgWidth: this.data.imgWidth * f,
imgHeight: this.data.imgHeight * f,
})
},
zoom
方法中的_calcXRatio
和_calcYRatio
两个方法是用来计算双指中心点处于图片的什么位置,这样做是为了得出后面marginLeft
和marginTop
需要的改变的值占图片宽高变化的多少,使得双指的中心点不动。方法实现如下:
/**
* 计算x轴上的双指中心点比例
*/
_calcXRatio: function (event) {
var xRatio = ((event.touches[0].clientX + event.touches[1].clientX) / 2 - this.data.marginLeft) / this.data.imgWidth
return xRatio
},
/**
* 计算y轴上的双指中心点比例
*/
_calcYRatio: function (event) {
var yRatio = ((event.touches[0].clientY + event.touches[1].clientY) / 2 - this.data.marginTop) / this.data.imgHeight
return yRatio
},
在计算出xRatio
和yRatio
之后,后面的两个if判断的用处是在图片已经处于最小状态时,继续缩小的话直接return
。而两个if后面的this.setData
则是真正的缩放图片,同时修改marginLeft
和marginTop
的值。
上面的就是触摸事件执行中的相关逻辑,接下来还有一些收尾工作。首先是catchtouchend='_touchEndEvent'
catchtouchcancel='_touchEndEvent'
的触摸结束事件。这个事件做的很简单,只是把inTouch
给改成了false
/**
* 触摸事件结束
*/
_touchEndEvent: function () {
inTouch = false
},
也就是一开始在图片加载完成后设置的定时器里面执行的方法_reboundAnimation()
,这个方法的原理很简单,就是不断的判断marginTop
和marginLeft
是否导致image
超出边界了,如果是则慢慢的移动回去。不过主要是因为每5ms执行一次,很吃CPU,还是有待优化啊。方法实现如下:
/**
* 边界的回弹动画
*/
_reboundAnimation: function () {
if (this.data.marginTop > 0) {
this.setData({
marginTop: this.data.marginTop - 4
})
if (this.data.marginTop - 4 < 0) {
this.setData({
marginTop: 0
})
}
}
if (this.data.marginLeft > 0) {
this.setData({
marginLeft: this.data.marginLeft - 4
})
if (this.data.marginLeft < 0) {
this.setData({
marginLeft: 0
})
}
}
if (this.data.marginLeft < 0 && (this.data.imgWidth - Math.abs(this.data.marginLeft)) < this.data.view_width) {
this.setData({
marginLeft: this.data.marginLeft + 4
})
}
if (this.data.marginTop < 0 && (this.data.imgHeight - Math.abs(this.data.marginTop)) < this.data.view_height) {
this.setData({
marginTop: this.data.marginTop + 4
})
}
},
https://download.csdn.net/download/xiao_wl/10293903