本人开发的svg编辑器原本只是针对 PC 端的。在pc端浏览器,可以正常地进行矢量图形地绘制,但在移动端无法使用,在画布上点击没有什么效果。但后来希望可以在一些屏幕大的移动设备上也能简单的使用,做了一些兼容方案,于是总结写下了这篇点击事件的pc端和移动端的适配处理方案分享。
基本架构
和 Adobe Illustrator 一样,这款 svg 编辑器也提供了很多工具。点击工具图标,就切换到对应的模式。
svg 编辑器目前支持下面工具:
- 选择工具
- 路径编辑工具
- 铅笔工具
- 钢笔(路径)工具
- 添加锚点工具
- 删除锚点工具
- 锚点工具
每个工具对应一个 action 对象。这个 action 对象有下面的属性(为了简明,这里我写成 ts 的接口形式,不过我 ts 不太熟,下面的语法可能会有问题,看懂就行):
interface EventHandler {
(e: object): void;
}
interface Action {
name: string; // action 的名称,如select,path等
bindEvents: string[]; // 字符串数组,记录需要绑定的事件名。如 mousedown
unbind: function // 解绑事件钩子
mousedown ?: EventHandler // 事件名:事件响应函数。
...
}
复制代码
举个例子:
let aciton: Action = {
name: 'select',
bindEvents: ['mousedown', 'mousemove', 'mouseup'],
unbind(){ /* 销毁在 select 模式下才会存在的对象 */}
mousedown(e) { /* 鼠标按下事件响应函数 */ },
mousemove(e) {},
mouseup(e) {},
}
复制代码
我们用一个名为 actionsManager.js
的文件来管理这些 action,来进行工具的切换。具体代码就不说了,简单说下它做了什么东西:
首先,我们会将这些 action
导入(即import)到该模块下,然后保存到一个 actions 对象中。名为 bindEvents 的函数负责 action 的切换。为了实现切换功能,我们还需要一个 curAction 变量,来指向当前正在使用的 action。
这样我们点击切换的时候,就可以通过遍历当前 action 的 bindEvents 数组的对应的所有事件响应函数,通过 removeEventListener
方法一一取消绑定。然后我们再遍历新的 action 的 bindEvents 数据,使用 addEventListener
来 绑定事件响应函数。
自此,编辑器的工具切换的实现大致说完。下面开始说明如何适配移动端的点击事件
移动端点击事件适配
在对代码进行改造前,我们先来了解一下移动端和pc端的点击行为的异同。移动端有独有的 touch 事件,虽然移动端是可以触发 mouse 事件,但和 pc 端有点不同。
PC端的点击行为
pc端是鼠标按下触发 mousedown 事件。移动鼠标的时候触发 mouseover 事件,但移动过程中,一旦光标离开了 event.srcElement
,就无法触发 mouseover 事件。鼠标释放则触发 mouseup 事件。
移动端的点击行为
手指按下时,会触发 touchstart 事件。此时手指不抬起进行移动的话,就会触发 touchmove 事件,即便移动到 event.srcElement
的范围外,依旧会触发 touchmove 事件,此时的 srcElemnt 依旧是原来的节点,而不是当前手指位置对应的节点。手指抬起时,不管此时手指在哪里,也和 touchmove 事件一样,srcElement 不会指向手指位置的节点。
移动端事件触发顺序
(1)移动端手指按下并提起(没有较大位移),依次触发以下事件:
- touchstart
- touchend
- mousemove
- mousedown
- mouseup
- click
(2)移动端手指按下并大幅移动,然后提起,触发的事件顺序:
- touchstart
- touchmove
- touchend
这里我们会看到,touchmove 事件的触发会导致 mouse 事件无法触发。
事实上,只要给 touchstart 或 touchend 事件添加一行 event.preventDefault()
,当发生 touch 行为时,mouse 事件就无法触发。touchmove 默认就能阻止 mouse 事件的触发,但在发生 touch 行为中,它不一定会触发(需要有较大偏移量)。
touchEvent
touchEvent 对象,是 touch 事件触发时产生的事件对象。它和 mouse 事件对象有一些不同的地方。首先手指的当前位置,是存放在一个名为 touches
的数组里的。该数组保存着 touch 对象。touch 对象有如 clentX, clientY 等很多坐标相关的属性,唯独没有 offsetX/offsetY 属性。
代码改造
下面我们就来对原有的代码进行改造。
移动端的 offsetX/offsetY 计算
touch 事件并不提供 offsetX 和 offsetY 属性。然而这两个属性对一款 svg 来说是必不可少的属性。我们需要用到这两个属性,来定位光标在画布上的位置,来绘制路径等图案。
为此我们需要写一个方法,将 pageX/pageY(光标距离页面左上角的位置)转换为 offsetX/offsetY,并作为 e 的补充属性。它们的关系是:
offsetX + left = pageX;
offsetY + top = pageY;
复制代码
left(top) 表示绑定 touch 事件的元素,距离页面左上角的距离。
const handleEventObj = function(e) {
const LEFT = 256; // 需要根据实际情况进行计算。
const TOP = 60;
const touch = e.changedTouches[0];
const offsetX = touch.pageX - LEFT + workarea.scrollLeft;
const offsetY = touch.pageY - TOP + workarea.scrollTop;
Object.assign(e, {offsetX, offsetY});
return e;
}
复制代码
改造工具切换函数
方案1:判断pc端还是移动端,决定是绑定 touch 事件还是 mouse 事件。
一开始我就是使用这种方案的,但它有一个问题。如果对于使用普通电脑的用户来说,这是没问题的,但如果用的是像 surface 这种可以触屏的笔记本,那就会有问题。因为通过触屏点击后,会触发 touch 事件,但 mouse 事件不一定会触发。即使触发了 mouse 事件,也是通过抬起手指触发的,而且是 mouseover -> mousedown -> mouseup 的顺序,且 mouseover 在手指按下移动过程中是无法触发的。
这种方案有很严重的问题,它无法胜任 触屏笔记本 的情况。
方案2:同时绑定 touch 事件和 mouse 事件
具体做法是,在 actionsManager.js
引入所有 action 的时候,就自动遍历所有 action 的 bindEvents。如果有 mousedown/mouseover/mouseup 事件响应函数,就额外添加对应的 touchstart/touchmove/touchend 事件响应函数。
具体代码如下:
const map = {
mousedown: 'touchstart',
mousemove: 'touchmove',
mouseup: 'touchend',
};
if (['mousedown', 'mousemove', 'mouseup'].includes(eventName)) {
action.bindEvents.push(map[eventName])
action[map[eventName]] = function(e) {
e.preventDefault(); // 这个很重要,它能阻止后续的 mouse 事件。
handleEventObj(e); // event 对象添加 offsetX ofsetY 属性。
action[eventName](e);
}
}
复制代码
这里的 eventName 就是事件名。
这里举个例子,假设我们有个 selectAction 对象,它有一个 mousedown 事件,我们就给 selectAction 添加一个 touchstart 方法。这个 touchstart 方法会阻止默认事件,这样就能阻止 mouse 事件触发,然后给 event 对象添加 offsetX/offsetY,把 event 传入 action.mousedown 方法。
这样,在 ipad 等移动端配合触屏笔,也可以进行简单的 svg 编辑操作了。
参考
掘金——touchstart与click不得不说的故事