uni-app图片手势处理
图片编辑里面最容易出问题的地方就是手势,单指拖动、双指缩放、双指旋转混在一起以后,逻辑就没有看起来那么简单了
做图片编辑时,最基础的交互就是拖动图片。
如果只是拖动,记录手指上一次的位置,然后计算差值就可以了。
但是头像编辑一般不止拖动,还需要缩放和旋转。
这时候就会出现一个问题:单指和双指操作要同时存在,而且不能互相干扰。
为什么会复杂
在 uni-app 里面,触摸事件里面会有 touches。
单指拖动时:
1 | e.touches.length === 1 |
双指缩放或旋转时:
1 | e.touches.length === 2 |
看起来只要判断手指数就行。
但是实际做的时候会遇到几个问题:
- 双指缩放结束后,可能还剩下一根手指停在屏幕上
- 双指旋转时,角度会出现从 180 到 -180 的跳变
- 图片已经旋转后,手指移动方向和图片坐标方向不一致
- 图片镜像后,左右拖动方向还要反过来
- 旋转图标拖动和普通图片拖动不能同时触发
所以组件里面维护了不少状态。
基础状态
组件里面记录了这些关键值:
1 | currentScale |
current 开头的是当前图片状态。
initial 开头的是手势开始时的状态。
这样在移动过程中,就可以用当前手势变化量加上初始状态,算出新的图片状态。
单指拖动
单指拖动的入口是图片区域:
1 | @touchstart="imghandleTouchStart" |
开始拖动时,先记录当前手指位置:
1 | this.lastTouch = e.touches[0] |
移动时计算差值:
1 | let dx = e.touches[0].clientX - this.lastTouch.clientX |
如果图片没有旋转,这样直接加到 currentTranslateX 和 currentTranslateY 就可以。
但是图片旋转以后就不行了。
旋转后的拖动
图片旋转以后,用户手指向右移动,图片自身坐标系里的方向不一定还是向右。
所以这里需要根据当前旋转角度做一次反向转换。
代码里面是这样处理的:
1 | const rotationAngle = this.currentRotation * Math.PI / 180 |
这里其实就是把屏幕上的移动距离,转换回图片自己的坐标系。
如果不这么处理,图片旋转以后拖动方向会很怪。
比如旋转 90 度以后,明明手指往右拖,图片可能会往上或者往下跑。
缩放后的拖动
还有一个点是缩放。
图片放大以后,页面上的 10px 移动,对图片自身来说不是 10px。
因为外层已经被 scale 放大了。
所以移动值还要除以当前缩放比例:
1 | this.currentTranslateX += transformedDx / this.currentScale |
这一步不处理的话,放大图片后拖动会变得特别快。
双指缩放和旋转
双指操作主要看两个值:
- 两根手指之间的距离
- 两根手指形成的角度
距离用来算缩放:
1 | this.currentScale = (currentDistance / this.startDistance) * this.initialScale |
角度用来算旋转:
1 | let angleDelta = currentAngle - this.startAngle |
这里有一个小坑,角度会在 180 和 -180 之间跳。
所以代码里面做了修正:
1 | if (angleDelta > 180) { |
这样旋转时就不会突然跳一大圈。
为什么要加旋转阈值
实际测试时,双指缩放很容易带出一点点旋转。
用户只是想放大图片,但是两根手指不可能完全保持水平,所以角度会有轻微变化。
如果一点角度变化都立刻触发旋转,体验会很飘。
所以组件里面加了一个判断:
1 | if (Math.abs(angleDelta) > 5) { |
大于 5 度以后才认为用户真的在旋转。
这个处理虽然简单,但是体验会稳定很多。
镜像后的方向问题
镜像也会影响手势。
图片水平翻转以后,左右方向其实反过来了。
所以拖动和旋转都要判断镜像状态:
1 | if (this.isMirrored == 1) { |
双指旋转时也要反过来:
1 | if (this.isMirrored == 1) { |
不处理这个的话,镜像后的图片操作会和手指方向对不上。
总结
图片手势处理看起来是拖一拖、捏一捏,但是实际要处理的是坐标系变化。
旋转会改变坐标方向。
缩放会改变移动比例。
镜像会改变左右方向。
uni-app 里面做这类交互时,最重要的是把当前图片状态记录清楚,然后每次触摸都基于状态去计算新的 transform。
以上就是我对 uni-app 图片手势处理的理解,如有错误,欢迎大佬指出。