交互优化|实现手势同时缩放 & 平移
干杂活也要有干杂活的价值,本来标题上想加个「优雅」,但想想算了,整体实现上并不算优雅,甚至有些难以理解。大部分时间还是花费在看懂之前的代码在干嘛,以及如何最小改动实现需求 ~
背景
最近笔者一直投入在各种移动端 Web 编辑器体验优化上,刚好有一个画布交互优化的需求:
- 在之前只能双指缩放画布、单指平移画布,而 App 原生编辑器可以双指同时缩放 + 平移画布。
- 增加画布在拖拽后归位的动画效果,提高用户体验。
整个过程还是有点意思,值得记录一下。
实现效果
代码解析
关键点总结
技术实现上有以下3个关键点:
- 使用腾讯出品的 alloyfinger 手势库提供各类手势监听能力。这库虽然很古早了,最后一次维护都6年前了,但依然好用 ~
- 使用 css
transfrom
效果实现跟随手势缩放和平移的操作。 - 在手势操作结束后,把平移和缩放结果换算后反馈给画布,然后使用 css
transition
动画效果实现拖拽归位的动画效果。
本文就基于以上3点进行代码解析,大佬们如果看了关键点说明能理解做了什么,就可以省流不往下看啦 ~
使用 alloyfinger
alloyfinger 提供的手势很全面,但有些手势并没有在 README 中说明,要看源码有完整的:
本文用到的是pinch
(缩放)和twoFingerPressMove
(双指按住移动)。
还有一点,alloyfinger 没有支持 Typescript,需要自行补充下d.ts
。
简易版本自取:
declare module 'alloyfinger' {
export type GestureEvent = TouchEvent & { zoom: number; angle: number; rotate: number };
export default class AlloyFinger {
constructor(element: HTMLElement, options: object);
on(eventName: string, callback: (event: GestureEvent) => void): void;
off(eventName: string, callback: Function): void;
destroy(): void;
}
}
使用上很简单,不废话了,直接看官方示例即可,注意的一点是挂载的组件销毁前记得去gestureDetecter.destroy()
。
缩放 & 平移手势
pinch
(缩放)和twoFingerPressMove
(双指按住移动)其实都是双指操作,所以它们是同时响应的,但返回的Event
对象不同。
我们还是用2个监听事件来分开处理:
this.gestureDetecter.on('touchStart', (e) => {
...
if (e.touches.length === 2) {
// 表示双指触摸
this.handleCanvasPinchStart(e);
this.handleCanvasTwoFingerPressMoveStart(e);
}
...
});
this.gestureDetecter.on('pinch', this.handleCanvasPinch);
this.gestureDetecter.on('twoFingerPressMove', this.handleCanvasTwoFingerPressMove);
this.gestureDetecter.on('touchEnd', (e) => {
...
if (e.defaultPrevented) return;
...
if (isInCanvasTwoFinger) {
// 双指触摸结束
this.handleCanvasTwoFingerEnd();
}
});
handleCanvasPinchStart
和handleCanvasTwoFingerPressMoveStart
是当双指开始触摸时,将各种记录变量初始化归位,例如:
/**
* 双指移动位置
*/
let twoFingerTranslate = { x: 0, y: 0 };
/**
* 双指缩放
*/
let twoFingerZoom = 0;
handleCanvasPinch
和handleCanvasTwoFingerPressMove
实时监听当前缩放比例和移动距离。
handleCanvasTwoFingerEnd
当双指触摸结束时,需要将 css 处理的缩放移动反馈给画布,修改当前画布位置。
tramsform
响应手势
响应手势这部分很好理解,毕竟手势上返回的Event
中有e.zoom
(缩放比例)和e.deltaX``e.deltaY
(当前手势移动的距离),直接看代码:
handleCanvasPinch(e) {
// 当前缩放比例
const zoom = e.zoom;
...
// 换算计算缩放的边缘
...
// 更新 transform
this.updateTransform();
},
handleCanvasTwoFingerPressMove(e) {
const nextX = twoFingerTranslate.x + e.deltaX;
const nextY = twoFingerTranslate.y + e.deltaY;
// 获取平移后的位置
twoFingerTranslate = { x: nextX, y: nextY };
...
// 更新 transform
this.updateTransform();
},
// 更新 transform 样式
updateTransform(hasScale = true) {
// 获取当前画布 div 实例
const { shell } = this.editor;
const scale = hasScale ? `scale(${twoFingerZoom}) ` : '';
// 更新 transform 样式
shell.style.transform = `${scale}translate(${twoFingerTranslate.x}px, ${twoFingerTranslate.y}px)`;
},
手势结束时动画效果
手势结束,除了修正画布位置外(本文不具体展开),还需要提供画布位置修正时有一个过渡动画,而不是突兀的闪回屏幕中心。
handleCanvasTwoFingerEnd() {
...
// 修正画布位置
...
// 清除 transform
this.$nextTick(() => {
this.clearTransformWithAnimation();
, 0);
},
clearTransformWithAnimation() {
const { shell } = this.editor;
this.updateTransform(false);
requestAnimationFrame(() => {
// 增加动画效果
shell.style.transition = 'transform 0.3s ease-out';
// 清理 transform,动画效果即为位置平移
shell.style.transform = '';
function handleTransitionEnd(event) {
if (event.propertyName === 'transform') {
shell.style.transition = '';
shell.removeEventListener('transitionend', handleTransitionEnd);
}
}
shell.addEventListener('transitionend', handleTransitionEnd);
});
},
其中有几点细节:
- 需要先清除可能存在的
this.updateTransform(false);
,缩放不能存在动画效果上,视觉上会出现变大又变小的效果。 - 要先让 css 清除
scale
生效,我们需要加上requestAnimationFrame
让后续操作在下一帧执行。 - 在动画结束后,需要清理画布示例上的 css 动画效果,防止造成画布样式污染,这里使用
addEventListener('transitionend')
监听即可,还需切记记得removeEventListener
防止内存泄漏导致的意外。
总结
虽然交互效果上跟原生已一致,但在体验上确实不如 App 原生手势丝滑。还是那句话,用 H5 实现的体验最多最多只能达到原生开发的 80%。这一点从 FPS 上也能看出来,在很多操作上帧数都远远达不到 60,别说现在越来越多的 120 帧高刷屏。

转载自:https://juejin.cn/post/7358368402794512434