uni-app图片手势处理


图片编辑里面最容易出问题的地方就是手势,单指拖动、双指缩放、双指旋转混在一起以后,逻辑就没有看起来那么简单了

做图片编辑时,最基础的交互就是拖动图片。

如果只是拖动,记录手指上一次的位置,然后计算差值就可以了。

但是头像编辑一般不止拖动,还需要缩放和旋转。

这时候就会出现一个问题:单指和双指操作要同时存在,而且不能互相干扰。

为什么会复杂

uni-app 里面,触摸事件里面会有 touches

单指拖动时:

1
e.touches.length === 1

双指缩放或旋转时:

1
e.touches.length === 2

看起来只要判断手指数就行。

但是实际做的时候会遇到几个问题:

  1. 双指缩放结束后,可能还剩下一根手指停在屏幕上
  2. 双指旋转时,角度会出现从 180 到 -180 的跳变
  3. 图片已经旋转后,手指移动方向和图片坐标方向不一致
  4. 图片镜像后,左右拖动方向还要反过来
  5. 旋转图标拖动和普通图片拖动不能同时触发

所以组件里面维护了不少状态。

基础状态

组件里面记录了这些关键值:

1
2
3
4
5
6
7
8
9
10
11
12
13
currentScale
currentRotation
currentTranslateX
currentTranslateY

initialScale
initialRotation
initialTranslateX
initialTranslateY

lastTouch
isRotating
isRotatingWithIcon

current 开头的是当前图片状态。

initial 开头的是手势开始时的状态。

这样在移动过程中,就可以用当前手势变化量加上初始状态,算出新的图片状态。

单指拖动

单指拖动的入口是图片区域:

1
2
3
@touchstart="imghandleTouchStart"
@touchmove="imghandleTouchMove"
@touchend="imghandleTouchEnd"

开始拖动时,先记录当前手指位置:

1
this.lastTouch = e.touches[0]

移动时计算差值:

1
2
let dx = e.touches[0].clientX - this.lastTouch.clientX
let dy = e.touches[0].clientY - this.lastTouch.clientY

如果图片没有旋转,这样直接加到 currentTranslateXcurrentTranslateY 就可以。

但是图片旋转以后就不行了。

旋转后的拖动

图片旋转以后,用户手指向右移动,图片自身坐标系里的方向不一定还是向右。

所以这里需要根据当前旋转角度做一次反向转换。

代码里面是这样处理的:

1
2
3
4
5
6
7
8
9
10
11
const rotationAngle = this.currentRotation * Math.PI / 180
const cos = Math.cos(rotationAngle)
const sin = Math.sin(rotationAngle)

const inverseRotationMatrix = [
[cos, sin],
[-sin, cos]
]

let transformedDx = dx * inverseRotationMatrix[0][0] + dy * inverseRotationMatrix[0][1]
let transformedDy = dx * inverseRotationMatrix[1][0] + dy * inverseRotationMatrix[1][1]

这里其实就是把屏幕上的移动距离,转换回图片自己的坐标系。

如果不这么处理,图片旋转以后拖动方向会很怪。

比如旋转 90 度以后,明明手指往右拖,图片可能会往上或者往下跑。

缩放后的拖动

还有一个点是缩放。

图片放大以后,页面上的 10px 移动,对图片自身来说不是 10px。

因为外层已经被 scale 放大了。

所以移动值还要除以当前缩放比例:

1
2
this.currentTranslateX += transformedDx / this.currentScale
this.currentTranslateY += transformedDy / this.currentScale

这一步不处理的话,放大图片后拖动会变得特别快。

双指缩放和旋转

双指操作主要看两个值:

  1. 两根手指之间的距离
  2. 两根手指形成的角度

距离用来算缩放:

1
this.currentScale = (currentDistance / this.startDistance) * this.initialScale

角度用来算旋转:

1
2
let angleDelta = currentAngle - this.startAngle
this.currentRotation = angleDelta + this.initialRotation

这里有一个小坑,角度会在 180-180 之间跳。

所以代码里面做了修正:

1
2
3
4
5
if (angleDelta > 180) {
angleDelta -= 360
} else if (angleDelta < -180) {
angleDelta += 360
}

这样旋转时就不会突然跳一大圈。

为什么要加旋转阈值

实际测试时,双指缩放很容易带出一点点旋转。

用户只是想放大图片,但是两根手指不可能完全保持水平,所以角度会有轻微变化。

如果一点角度变化都立刻触发旋转,体验会很飘。

所以组件里面加了一个判断:

1
2
3
if (Math.abs(angleDelta) > 5) {
this.canRotation = true
}

大于 5 度以后才认为用户真的在旋转。

这个处理虽然简单,但是体验会稳定很多。

镜像后的方向问题

镜像也会影响手势。

图片水平翻转以后,左右方向其实反过来了。

所以拖动和旋转都要判断镜像状态:

1
2
3
if (this.isMirrored == 1) {
dx = -dx
}

双指旋转时也要反过来:

1
2
3
if (this.isMirrored == 1) {
angleDelta = -angleDelta
}

不处理这个的话,镜像后的图片操作会和手指方向对不上。

总结

图片手势处理看起来是拖一拖、捏一捏,但是实际要处理的是坐标系变化。

旋转会改变坐标方向。

缩放会改变移动比例。

镜像会改变左右方向。

uni-app 里面做这类交互时,最重要的是把当前图片状态记录清楚,然后每次触摸都基于状态去计算新的 transform。

以上就是我对 uni-app 图片手势处理的理解,如有错误,欢迎大佬指出。

-------------本文结束感谢您的阅读-------------