我们上一期已经实现了所有的 gesture(手势),接下来我们需要实现的就是事件派发的功能。
在 DOM 里面事件的派发是使用 new Event , 然后在上面加一些属性,最后把这个事件给派发出去的。
所以我们这里也是一样,建立一个 dsipatch
的函数,并且加入 type
、property
这些参数。这里的 property 含有 context 对象和 point 坐标两个属性。
在我们的 dispatch
函数中,首先我们需要做的就是创建一个 event 对象。在新的浏览器 API 中,我们可以直接使用 new Event
来创建。当然我们也可以使用自定义事件来创建 new CustomEvent
。那么我们这里,就用普通的 new Event
就好了。
function dispatch(type, properties) {
let event = new Event(type);
}
然后我们循环一下 properties
这个对象,把里面的属性都抄写一下。然后我们新创建的 event 是需要挂在一个元素上面,把它挂在到我们之前定义的 element
上即可。
function dispatch(type, properties) {
let event = new Event(type);
for (let name in properties) {
event[name] = properties[name];
}
element.dispatchEvent(event);
}
这里其实还有一个问题,就是我们之前写的监听都是挂载在 element
之上的。最后我们要把这些都换成挂载在 document
上。
element.addEventListener('mousedown', event => {
let context = Object.create(null);
contexts.set(`mouse${
1 << event.button}`, context);
start(event, context);
let mousemove = event => {
let button = 1;
while (button <= event.buttons) {
if (button & event.buttons) {
let key;
// Order of buttons & button is not the same
if (button === 2) {
key = 4;
} else if (button === 4) {
key = 2;
} else {
key = button;
}
let context = contexts.get('mouse' + key);
move(event, context);
}
button = button << 1;
}
};
let mouseup = event => {
let context = contexts.get(`mouse${
1 << event.button}`);
end(event, context);
contexts.delete(`mouse${
1 << event.button}`);
if (event.buttons === 0) {
document.removeEventListener('mousemove', mousemove);
document.removeEventListener('mouseup', mouseup);
isListeningMouse = false;
}
};
if (!isListeningMouse) {
document.addEventListener('mousemove', mousemove);
document.addEventListener('mouseup', mouseup);
isListeningMouse = true;
}
});
然后我们来把 end 函数中的 tap
事件 dipatch(派发)出来试试:
let end = (point, context) => {
if (context.isTap) {
//console.log('tap');
// 把原先的 console.log 换成 dispatch 调用
// 这个事件不需要任何特殊属性,直接传`空对象`即可
dispatch('tap', {
})
clearTimeout(context.handler);
}
if (context.isPan) {
console.log('pan-end');
}
if (context.isPress) {
console.log('press-end');
}
};
那么最后,我们可以尝试在 HTML 中加入一个脚本,在里面监听一下我们新创建的 tap
事件。
<script src="gesture.js">script>
<body oncontextmenu="event.preventDefault()">body>
<script>
document.documentElement.addEventListener('tap', () => {
console.log('Tapped!');
});
script>
这个时候,如果我们去浏览器上点击一下,就会触发我们的 tap
事件,并且输出我们的 'Tapped'
消息了!
这样我们的派发事件就大功告成了。
这里我们一起来完成最后一个最特别的 flick 事件。Flick 事件在我们所有的事件体系里是比较特殊的,因为它是一个需要判断数独的一个事件。
根据我们前面讲到的,在 pan start
之后,如果我们在手指离开屏幕之前,我们执行了一个快速滑动手指的动作,到达一定的速度以上就会触发我们的 flick
事件,而不是原本的 pan end
的事件。
那么需要如何判断这个速度的?其实可以在我们的 move 函数中,获得当前这一次移动时的速度。但是这个并不能帮助我们去处理,因为如果只按照两个点之间移动时的速度,根据浏览器实现的不同,它会有一个较大的误差。
所以更加准确的方式就是,取数个点,然后用它们之间的平均值作为判定的值。那么要实现这个功能,我们就需要存储一段时间之内的这些点,然后使用这些点来计算出速度的平均值。
有了实现的思路了,我们就来整理下,在代码中怎么去编写这一块的逻辑。
首先我们需要在触发 start 的时候,就把第一个记录点加入到我们的全局 context
之中。而这里需要记录几个值:
t
:代表当前点触发/加入时的时间,这里我们使用 Date.now()
x
:代表当前点 x 轴的坐标y
:代表当前点 y 轴的坐标这些值到了后面都会用来计算移动速度的。
let start = (point, context) => {
(context.startX = point.clientX), (context.startY = point.clientY);
context.points = [
{
t: Date.now(),
x: point.clientX,
y: point.clientY,
},
];
context.isPan = false;
context.isTap = true;
context.isPress = false;
context.handler = setTimeout(() => {
context.isPan = false;
context.isTap = false;
context.isPress = true;
console.log('press-start');
context.handler = null;
}, 500);
};
然后每一次触发 move 的时候,都给当前的 content 放入一个新的点。但是在加入新的点之前,需要过滤一次已经存储的点。我们只需要最近 500 毫秒内的点来计算速度即可,其余的点就可以过滤掉了。
在执行 flick 动作的时候,我们是不会滑动一个很长的距离和时间的,加上我们是需要捕捉一个快速的滑动动作,这个动作肯定是在 500 毫秒以内的动作,要不也不叫 “快” 了。所以这里就只需要 500 毫秒内的点即可。
let move = (point, context) => {
let dx = point.clientX - context.startX,
dy = point.clientY - context.startY;
if (!context.isPan && dx ** 2 + dy ** 2 > 100) {
context.isPan = true;
context.isTap = false;
context.isPress = false;
console.log('pan-start');
clearTimeout(context.handler);
}
if (context.isPan) {
console.log(dx, dy);
console.log('pan');
}
context.points = context.points.filter(point => Date.now() - point.t < 500);
context.points.push({
t: Date.now(),
x: point.clientX,
y: point.clientY,
});
};
在 end 事件触发的时候,就可以来计算这次滑动的速度了。因为这里是计算用户滑动时的速度,如果用户是其他类型的手势动作,是不需要去计算速度的。所以这段计算逻辑就可以写在 isPan
成立的判断里面即可。
首先给这个手势动作一个状态变量 isFlick
,并且给予它一个默认值为 false
。
在计算速度之前,一样需要过滤一次我们 context 中储存的全部的点,把 500 毫秒之外的点过滤掉。
在数学或者物理中,有一个计算速度的公式: 速度 = 距离 / 用时
。那么这里要去计算速度的话,首先需要计算的就是距离。而这里要计算的是直径距离,所以需要 x 轴和 y 轴的距离的二次幂相加,然后开根号获得的值就是我们要的直径距离。
那么 x 轴距离为例,就是当前点的 x 轴坐标,减去记录中第一个点的 x 轴左边。y 轴的距离就同理可得了。那么有了距离,我们就可以直接从当前点和第一个点的时间差获得 用时
。最后就可以运算出速度。
let end = (point, context) => {
context.isFlick = false;
if (context.isTap) {
//console.log('tap');
// 把原先的 console.log 换成 dispatch 调用
// 这个事件不需要任何特殊属性,直接传`空对象`即可
dispatch('tap', {
});
clearTimeout(context.handler);
}
if (context.isPan) {
context.points = context.points.filter(point => Date.now() - point.t < 500);
let d = Math.sqrt((point.x - context.points[0].x) ** 2 + (point.y - context.points[0].y) ** 2);
let v = d / (Date.now() - context.points[0].t);
}
if (context.isPress) {
console.log('press-end');
}
};
好样的,这样我们就有两个点之间的 v
速度。那么现在呢,我们需要知道多快的速度才能认为是一个 flick 动作呢?这里就用上帝视角直接得出 1.5 像素每毫秒的速度就是最合适的(这个怎么算出来的?其实我们可以直接 console.log(v),把速度打印出啦,然后我们手动去测试,就会发现大概 v = 1.5 的时候差不多就是对的了)。
所以我们这里直接就可以判断, 如果 v > 1.5 的话,我们就认为用户的手势就是一个 flick,否则就是普通的 pan-end。
let end = (point, context) => {
context.isFlick = false;
if (context.isTap) {
//console.log('tap');
// 把原先的 console.log 换成 dispatch 调用
// 这个事件不需要任何特殊属性,直接传`空对象`即可
dispatch('tap', {
});
clearTimeout(context.handler);
}
if (context.isPan) {
context.points = context.points.filter(point => Date.now() - point.t < 500);
let d = Math.sqrt((point.x - context.points[0].x) ** 2 + (point.y - context.points[0].y) ** 2);
let v = d / (Date.now() - context.points[0].t);
if (v > 1.5) {
context.isFlick = true;
dispatch('flick', {
});
} else {
context.isFlick = false;
dispatch('panend', {
});
}
}
if (context.isPress) {
console.log('press-end');
}
};
这样 flick 事件的处理就完成了,其实这段代码中还有一些 console.log() 是没有被改为使用 dispatch 给派发出去的。但是接下来就要开始看看怎么重新封装这个手势库了,所以这里我们就不一一更改过来先了。
如果想把这里的代码写完整的同学,可以自行把所有的 console.log(事件名) 部分的代码都改正过来哦~
最后附上到此完整的代码。
let element = document.documentElement;
let contexts = new Map();
let isListeningMouse = false;
element.addEventListener('mousedown', event => {
let context = Object.create(null);
contexts.set(`mouse${
1 << event.button}`, context);
start(event, context);
let mousemove = event => {
let button = 1;
while (button <= event.buttons) {
if (button & event.buttons) {
let key;
// Order of buttons & button is not the same
if (button === 2) {
key = 4;
} else if (button === 4) {
key = 2;
} else {
key = button;
}
let context = contexts.get('mouse' + key);
move(event, context);
}
button = button << 1;
}
};
let mouseup = event => {
let context = contexts.get(`mouse${
1 << event.button}`);
end(event, context);
contexts.delete(`mouse${
1 << event.button}`);
if (event.buttons === 0) {
document.removeEventListener('mousemove', mousemove);
document.removeEventListener('mouseup', mouseup);
isListeningMouse = false;
}
};
if (!isListeningMouse) {
document.addEventListener('mousemove', mousemove);
document.addEventListener('mouseup', mouseup);
isListeningMouse = true;
}
});
element.addEventListener('touchstart', event => {
for (let touch of event.changedTouches) {
let context = Object.create(null);
contexts.set(event.identifier, context);
start(touch, context);
}
});
element.addEventListener('touchmove', event => {
for (let touch of event.changedTouches) {
let context = contexts.get(touch.identifier);
move(touch, context);
}
});
element.addEventListener('touchend', event => {
for (let touch of event.changedTouches) {
let context = contexts.get(touch.identifier);
end(touch, context);
contexts.delete(touch.identifier);
}
});
element.addEventListener('cancel', event => {
for (let touch of event.changedTouches) {
let context = contexts.get(touch.identifier);
cancel(touch, context);
contexts.delete(touch.identifier);
}
});
let start = (point, context) => {
(context.startX = point.clientX), (context.startY = point.clientY);
context.points = [
{
t: Date.now(),
x: point.clientX,
y: point.clientY,
},
];
context.isPan = false;
context.isTap = true;
context.isPress = false;
context.handler = setTimeout(() => {
context.isPan = false;
context.isTap = false;
context.isPress = true;
console.log('press-start');
context.handler = null;
}, 500);
};
let move = (point, context) => {
let dx = point.clientX - context.startX,
dy = point.clientY - context.startY;
if (!context.isPan && dx ** 2 + dy ** 2 > 100) {
context.isPan = true;
context.isTap = false;
context.isPress = false;
console.log('pan-start');
clearTimeout(context.handler);
}
if (context.isPan) {
console.log(dx, dy);
console.log('pan');
}
context.points = context.points.filter(point => Date.now() - point.t < 500);
context.points.push({
t: Date.now(),
x: point.clientX,
y: point.clientY,
});
};
let end = (point, context) => {
context.isFlick = false;
if (context.isTap) {
//console.log('tap');
// 把原先的 console.log 换成 dispatch 调用
// 这个事件不需要任何特殊属性,直接传`空对象`即可
dispatch('tap', {
});
clearTimeout(context.handler);
}
if (context.isPan) {
context.points = context.points.filter(point => Date.now() - point.t < 500);
let d, v;
if (!context.points.length) {
v = 0;
} else {
d = Math.sqrt(
(point.clientX - context.points[0].x) ** 2 + (point.clientY - context.points[0].y) ** 2
);
v = d / (Date.now() - context.points[0].t);
}
if (v > 1.5) {
context.isFlick = true;
dispatch('flick', {
});
} else {
context.isFlick = false;
dispatch('panend', {
});
}
}
if (context.isPress) {
console.log('press-end');
}
};
let cancel = (point, context) => {
clearTimeout(context.handler);
console.log('cancel');
};
function dispatch(type, properties) {
let event = new Event(type);
for (let name in properties) {
event[name] = properties[name];
}
element.dispatchEvent(event);
}
下一期,我们就来做手势库的最后一步,封装!~
我是来自《技术银河》的三钻,一位正在重塑知识的技术人。下期再见。
在最近在版本 1.5.0 更新了以下功能:
》预览《
✨ 新增
置顶文章布局
” !!)
Info
容器Warning
容器Danger
容器Detail
容器description
keywords
author
最近博主在全面投入开发一个可以 “迈向未来的” Hexo 主题,以极光为主题的博客主题。
如果你是一个开发者,做一个个人博客也是你简历上的一个亮光点。而如果你有一个超级炫酷的博客,那就更加是亮上加亮了,简直就闪闪发光。
如果喜欢这个主题,可以在 Github 上给我点个 让彼此都发光吧~
主题 Github 地址:https://github.com/auroral-ui/hexo-theme-aurora
主题使用文档:https://aurora.tridiamond.tech/zh/
对,博主还做了一个 Aurora 的 VSCode 主题。用了Hexo Theme Aurora 相对应的颜色色系。这个主题的重点特性的就只用了 3 个颜色,减少在写代码的时候被多色多彩的颜色所转移了你的注意力,让你更集中在写代码之中。
喜欢的大家可以支持一下哦! 直接在 VSCode 的插件搜索中输入 “Aurora Future” 即可找到这个主题哦!~
主题 Github 地址:https://github.com/auroral-ui/aurora-future-vscode-theme
主题插件地址:https://marketplace.visualstudio.com/items?itemName=auroral-ui.aurora-future
我不知道大家,但是最近我在用火狐浏览器来做开发了。个人觉得火狐还真的是不错的。推荐大家尝试一下。
当然我这里想给大家介绍的是我在火狐也做了一个 Aurora 主题。对的!用的是同一套的颜色体系。喜欢的小伙伴可以试一下哦!
主题地址:https://addons.mozilla.org/en-US/firefox/addon/aurora-future/