介绍

微信自带组件movable-area/movable-view可以实现图片的移动及缩放功能,不过其缩放中心是图片的中心(仅可通过transform-origin调节)

我们希望能够以触摸点作为缩放中心进行缩放,同时为了获得更多的控制,我们选择自己实现此功能

需求

1.图片在一定范围内移动
2.图片以触摸点作为缩放中心进行缩放

实现

wxml布局

通过修改transform的属性,达到移动和缩放的效果

1
2
3
4
5
6
7
8
9
10
11
12
<view class="container">
<view
id="map-area" class="map-area"
bindtouchstart="touchStart"
bindtouchmove="touchMove"
bindtouchend="touchEnd">
<image
id="map-bg" class="map-bg" src="/images/map-indoor-bg.jpg"
style="transform: translateX({{mapLeft}}px) translateY({{mapTop}}px) translateZ(0px) scale({{mapScale}}); will-change: transform;"
></image>
</view>
</view>

wxss(使用了scss)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
page {
width: 100%;
height: 100%;
}

.container {
width: 100%;
height: 100%;
.map-area {
width: 100%;
height: 100%;
overflow: hidden;
background-color: yellow;
$map-width: rpx(1700);
$map-height: rpx(1400);
.map-bg {
transform-origin: 0 0; // 缩放中心设置为(0,0),便于程序计算
width: $map-width;
height: $map-height;
box-sizing: border-box;
border: 1px solid red;
}
}
}

js部分(…表示缩略了部分代码)

需要记录如下信息,供移动和缩放计算使用,同时在加载时获取map-bg和map-area的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Page({
data: {
// map-area的初始大小
mapAreaTop: 0,
mapAreaWidth: 0,
mapAreaHeight: 0,
// map-bg的初始大小
originalMapWidth: 0,
originalMapHeight: 0,
// 缩放后的map-bg的信息
mapScale: 1,
mapWidth: 0,
mapHeight: 0,
// 移动后的map-bg的信息
mapLeft: 0,
mapTop: 0,
// 触摸触发时,map-bg的信息
initMapScale: 1,
initTouchX: 0, // 手指距离map-bg左上角(0,0)的距离
initTouchY: 0,
initMapLeft: 0,
initMapTop: 0,
initGap: 0 // 两指初始间距
},
onLoad: function () {
// 获取map-area大小
wx.createSelectorQuery().select('#map-area').boundingClientRect((rect) => {
this.data.mapAreaWidth = rect.width // 节点的宽度
this.data.mapAreaHeight = rect.height // 节点的高度
this.data.mapAreaTop = rect.top // 节点的top
// P.S. 如果map-area不紧贴左边缘,还需要获取rect.left,并在获取touchX时减去,以保证缩放中心位置准确
}).exec()
// 获取map-bg大小
wx.createSelectorQuery().select('#map-bg').boundingClientRect((rect) => {
this.data.mapWidth = rect.width // 节点的宽度
this.data.mapHeight = rect.height // 节点的高度
this.data.originalMapWidth = rect.width // 节点的宽度
this.data.originalMapHeight = rect.height // 节点的高度
}).exec()
}
...
})

图片移动

经过实验,我们发现当触摸点增加时会触发touchStart事件,当触摸点减少时会触发touchEnd事件。
所以我们创建一个触摸初始化函数,用于记录单指或双指触摸的初始状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Page({
...
// 初始化单指触摸
oneTouchStart: function (e) {
// 获取触摸位置
let touchX = e.touches[0].pageX
let touchY = e.touches[0].pageY - this.data.mapAreaTop
// 赋值
this.setData({
'initTouchX': touchX,
'initTouchY': touchY,
'initMapLeft': this.data.mapLeft,
'initMapTop': this.data.mapTop
})
}
...
})

之后在touchStart/touchEnd事件增加初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Page({
...
touchStart: function (e) {
// 处理单指移动
if (e.touches.length === 1) {
this.oneTouchStart(e)
}
...
},
touchEnd: function (e) {
// 处理单指移动
if (e.touches.length === 1) {
this.oneTouchStart(e)
}
...
}
...
})

在touchMove事件中处理图片的移动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Page({
...
touchMove: function (e) {
// 处理单指移动
if (e.touches.length === 1) {
// 获取触摸位置
let touchX = e.touches[0].pageX
let touchY = e.touches[0].pageY - this.data.mapAreaTop
// 移动的距离
let mapMovedLeft = touchX - (this.data.initTouchX - this.data.initMapLeft)
let mapMovedTop = touchY - (this.data.initTouchY - this.data.initMapTop)
// 限制map-bg左上角(0,0)移动距离
let limitRight = 0
let limitBottom = 0
let limitLeft = -(this.data.mapWidth - this.data.mapAreaWidth)
let limitTop = -(this.data.mapHeight - this.data.mapAreaHeight)
if (mapMovedLeft > limitRight) {
mapMovedLeft = limitRight
// 在到达边缘时实时刷新触摸点初始位置,借此使反向移动时图片能立即移动
this.setData({
'initMapLeft': mapMovedLeft,
'initTouchX': touchX
})
}
if (mapMovedTop > limitBottom) {
mapMovedTop = limitBottom
this.setData({
'initMapTop': mapMovedTop,
'initTouchY': touchY
})
}
if (mapMovedLeft < limitLeft) {
mapMovedLeft = limitLeft
this.setData({
'initMapLeft': mapMovedLeft,
'initTouchX': touchX
})
}
if (mapMovedTop < limitTop) {
mapMovedTop = limitTop
this.setData({
'initMapTop': mapMovedTop,
'initTouchY': touchY
})
}
// 赋值
this.setData({
'mapLeft': parseInt(mapMovedLeft),
'mapTop': parseInt(mapMovedTop)
})
}
...
}
...
})

图片缩放

创建缩放(双指触摸)初始化函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Page({
...
// 初始化双指触摸
twoTouchStart: function (e) {
// 获取两指距离
let xGap = e.touches[1].pageX - e.touches[0].pageX
let yGap = e.touches[1].pageY - e.touches[0].pageY
let gap = Math.sqrt(xGap * xGap + yGap * yGap)
// 获取触摸中心
let touchX = (e.touches[0].pageX + e.touches[1].pageX) / 2
let touchY = (e.touches[0].pageY + e.touches[1].pageY) / 2 - this.data.mapAreaTop
// 赋值
this.setData({
'initGap': gap,
'initMapScale': this.data.mapScale,
'initMapLeft': this.data.mapLeft,
'initMapTop': this.data.mapTop,
'initTouchX': touchX,
'initTouchY': touchY
})
},
...
})

在touchStart/touchEnd事件增加初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Page({
...
touchStart: function (e) {
...
// 处理双指缩放
if (e.touches.length === 2) {
this.twoTouchStart(e)
}
},
touchEnd: function (e) {
...
// 处理双指缩放
if (e.touches.length === 2) {
this.twoTouchStart(e)
}
}
...
})

在touchMove事件中处理图片的缩放

这里我们需要处理一下偏移,以保证缩放中心是触摸位置,下图红线部分则是我们需要计算出的偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
Page({
...
touchMove: function (e) {
...
// 处理双指动作
if (e.touches.length === 2) {
// 获取两指距离
let xGap = e.touches[1].pageX - e.touches[0].pageX
let yGap = e.touches[1].pageY - e.touches[0].pageY
let gap = Math.sqrt(xGap * xGap + yGap * yGap)
// 获取触摸中心
let touchX = parseInt((e.touches[0].pageX + e.touches[1].pageX) / 2)
let touchY = parseInt((e.touches[0].pageY + e.touches[1].pageY) / 2 - this.data.mapAreaTop)
// 计算实时缩放比例
let mapScale = this.data.initMapScale + (gap - this.data.initGap) * 0.01
console.log(this.data.initGap, gap, ',', this.data.initMapScale, mapScale)
// 限制缩小/放大的比例
let limitMinWidthScale = this.data.mapAreaWidth / this.data.originalMapWidth // 宽度最小能缩放的比例
let limitMinHeightScale = this.data.mapAreaHeight / this.data.originalMapHeight // 高度最小能缩放的比例
let limitMinSettingScale = 0.1 // 自定义最小缩放比例
let limitMinScale = Math.max(limitMinWidthScale, limitMinHeightScale, limitMinSettingScale) // 取三者最大值
let limitMaxSettingScale = 10 // 自定义最大缩放比例
if (mapScale < limitMinScale) {
mapScale = limitMinScale
}
if (mapScale > limitMaxSettingScale) {
mapScale = limitMaxSettingScale
}
// 计算缩放后宽高
let mapWidth = this.data.originalMapWidth * mapScale
let mapHeight = this.data.originalMapHeight * mapScale
// 计算距离触摸中心的偏移
let initMapWidth = this.data.originalMapWidth * this.data.initMapScale
let initMapHeight = this.data.originalMapHeight * this.data.initMapScale
// 原Left - (现在Width - 初始Width)* (初始触摸点到初始Left距离 / 初始宽度)
let mapMovedLeft = this.data.initMapLeft - ((mapWidth - initMapWidth) * (this.data.initTouchX - this.data.initMapLeft) / initMapWidth)
let mapMovedTop = this.data.initMapTop - ((mapHeight - initMapHeight) * (this.data.initTouchY - this.data.initMapTop) / initMapHeight)
// 限制map-bg左上角(0,0)移动距离
let limitRight = 0
let limitBottom = 0
let limitLeft = -(mapWidth - this.data.mapAreaWidth)
let limitTop = -(mapHeight - this.data.mapAreaHeight)
if (mapMovedLeft > limitRight) {
mapMovedLeft = limitRight
}
if (mapMovedTop > limitBottom) {
mapMovedTop = limitBottom
}
if (mapMovedLeft < limitLeft) {
mapMovedLeft = limitLeft
}
if (mapMovedTop < limitTop) {
mapMovedTop = limitTop
}
// 赋值
this.setData({
'mapScale': mapScale,
'mapWidth': mapWidth,
'mapHeight': mapHeight,
'mapLeft': parseInt(mapMovedLeft),
'mapTop': parseInt(mapMovedTop)
})
}
}
})

优化安卓端体验

经过实际测试发现安卓端存在卡顿现象,增加限流函数,以提高体验
同时将wxml改成bindtouchmove=”throttleTouchMove”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Page({
...
onLoad: function () {
// 设置限流函数
this.throttleTouchMove = this.throttle(this.touchMove, 20, 40)
...
},
// 限流函数
throttle: function (func, wait, mustRun) {
let timeout
let startTime = new Date()

return function () {
let context = this
let args = arguments
let curTime = new Date()
clearTimeout(timeout)
if (curTime - startTime >= mustRun) {
// 如果达到了规定的触发时间间隔,触发 handler
func.apply(context, args)
startTime = curTime
} else {
// 没达到触发间隔,重新设定定时器
timeout = setTimeout(func, wait, ...args)
}
}
}
...
})