目录
- 演示
- 原理
- touch对象
- touch事件
- targetTouches和touches的区别
- 实现(代码部分)
演示
第1个交互是点击弹出隐藏按钮
第2个交互是长按“百度”按钮,然后弹出隐藏按钮,然后保持长按向上移动,触点经过的按钮会有特效,松开就会选定并跳转。
原理
通过对touchstart、touchmove、touchend事件监听,实现长按响应,然后通过计算位移来确定用户的手指在哪个按钮上面,当手指松开时,则触发跳转,因为要根据位移来确定触碰点在哪个按钮上面,所以需要确定位移多少后属于哪个按钮的范围,也就要知道每个按钮的高度。
touch对象
Touch构造函数接受一个配置对象作为参数,它有以下属性。
identifier:必需,类型为整数,表示触摸点的唯一 ID。
target:必需,类型为元素节点,表示触摸点开始时所在的网页元素。
clientX:可选,类型为数值,表示触摸点相对于浏览器窗口左上角的水平距离,默认为0。
clientY:可选,类型为数值,表示触摸点相对于浏览器窗口左上角的垂直距离,默认为0。
screenX:可选,类型为数值,表示触摸点相对于屏幕左上角的水平距离,默认为0。
screenY:可选,类型为数值,表示触摸点相对于屏幕左上角的垂直距离,默认为0。
pageX:可选,类型为数值,表示触摸点相对于网页左上角的水平位置(即包括页面的滚动距离),默认为0。
pageY:可选,类型为数值,表示触摸点相对于网页左上角的垂直位置(即包括页面的滚动距离),默认为0。
radiusX:可选,类型为数值,表示触摸点周围受到影响的椭圆范围的 X 轴半径,默认为0。
radiusY:可选:类型为数值,表示触摸点周围受到影响的椭圆范围的 Y 轴半径,默认为0。
rotationAngle:可选,类型为数值,表示触摸区域的椭圆的旋转角度,单位为度数,在0到90度之间,默认值为0。
force:可选,类型为数值,范围在0到1之间,表示触摸压力。0代表没有压力,1代表硬件所能识别的最大压力,默认为0。
关于touch事件
触摸事件与鼠标事件类似,不同的是触摸事件还提供同一表面不同位置的同步触摸。TouchEvent 接口将当前所有活动的触摸点封装起来。Touch 接口表示单独一个触摸点,其中包含参考浏览器视角的相对坐标。 ——《MDN Web Docs》
TouchEvent属性列表(部分)
TouchEvent.changedTouches 只读
一个 TouchList 对象,包含了代表所有从上一次触摸事件到此次事件过程中,状态发生了改变的触点的 Touch 对象,这里的状态改变常见的有其各种坐标(clientX、pageX、screenX等)或者触摸压力发生了改变,即触点移动了,返回的是原来的一开始的触点touch对象,但是它的一些属性发生了变化。
例子如下:
<!DOCTYPE html>
<html>
<head>
<title>targetTouches和touches的区别</title>
<style>
.layer1{
margin:20px;
600px;
height:600px;
display: flex;
justify-content: center;
align-items: center;
border: solid 1px;
}
.layer2{
margin:20px;
400px;
height:400px;
display: flex;
justify-content: center;
align-items: center;
border: solid 1px;
}
.layer3{
margin:20px;
200px;
height:200px;
border: solid 1px;
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>
<body>
<div id="container">
<div class="layer1">
layer1
<div class="layer2">
layer2
<div class="layer3">layer3</div>
</div>
</div>
</div>
</body>
<script>
let goStart = function(ev){
console.log("start======");
console.log("touches",ev.touches);
console.log("targetTouches",ev.targetTouches);
console.log("changedTouches",ev.changedTouches);
console.log(ev.touches.length === ev.targetTouches.length);
};
let goEnd = function(ev){
console.log("end======");
console.log("touches",ev.touches);
console.log("targetTouches",ev.targetTouches);
console.log("changedTouches",ev.changedTouches);
console.log(ev.touches.length === ev.targetTouches.length);
};
let layers = [].slice.call(document.querySelectorAll("#container div"));
layers[1].addEventListener("touchstart",goStart);
layers[1].addEventListener("touchend",goEnd);
</script>
</html>
我的代码给div.layer2的touchstart、touchmove和touchend事件都添加了监听器,打印一下touches、targetTouches和changedTouches三个属性,现在我只是点击一下layer3区域,打印结果如下:
点开changedTouches,可以看到,force属性值改变了,导致点击的触点touch对象被记录下来了。
相应的,当触点位置移动时,changedTouches也会记录。当从layer2移动到layer1范围,记录的还是一开始的那个触点,只不过其属性会随触点移动发生变化:
这里是从“layer2”字符串位置平移到“layer1”的位置,可以看见changedTouches记录的touch对象的target指向了div.layer2
总结:
changedTouches返回一个TouchList,其成员是在本次触摸事件中属性值发生变化的touch对象,该触摸事件包括了touchstart(force变为1)、touchmove(坐标改变)、touchend(force变为0,坐标也可能改变)。可以理解为只要发生触摸事件,changedTouches返回的TouchList一定不为空。
TouchEvent.targetTouches 只读
一个 TouchList 对象,是包含了如下触点的 Touch 对象:触摸起始于当前事件的目标 element 上,并且仍然没有离开触摸平面的触点。
该属性返回一个TouchList实例,成员是触摸事件的目标元素节点内部、所有仍然处于活动状态(即触摸中)的触摸点。
function touches_in_target(ev) {
return (ev.touches.length === ev.targetTouches.length ? true : false);
}
//上面代码用来判断,是否所有触摸点都在目标元素内
TouchEvent.touches 只读
一 个 TouchList 对象,包含了所有当前接触触摸平面的触点的 Touch 对象,无论它们的起始于哪个 element 上,也无论它们状态是否发生了变化。代表所有的当前处于活跃状态的触摸点,该属性返回一个TouchList实例,成员是所有仍然处于活动状态(即触摸中)的触摸点。一般来说,一个手指就是一个触摸点。
targetTouches和touches的对比
touches:当前屏幕上所有触摸点的集合列表
targetTouches: 绑定事件的那个结点上的触摸点的集合列表
changedTouches: 触发事件时改变的触摸点的集合
举例来说,比如div1, div2只有div2绑定了touchstart事件,第一次放下一个手指在div2上,触发了touchstart事件,这个时候,三个集合的内容是一样的,都包含这个手指的touch,然后,再放下两个手指一个在div1上,一个在div2上,这个时候又会触发事件,但changedTouches里面只包含第二个第三个手指的信息,因为第一个没有发生变化,而targetTouches包含的是在第一个手指和第三个在div2上的手指集合,touches包含屏幕上所有手指的信息,也就是三个手指。
参考:https://segmentfault.com/q/1010000002870710
补充:这三个属性记录的touch对象的target都是触发touchstart的那个元素,不管最后是不是移动到了别的元素上面。
实现
自适应
这里采用的是rem作为单位的自适应方案,限制最大宽度。
// 设置基准大小
const baseSize =16;
function setRem () {
// 当前页面宽度相对于 414 宽(设计图)的缩放比例
const scale = document.documentElement.clientWidth / 414
document.documentElement.style.fontSize = Math.min(29.6,(baseSize * Math.min(scale, 2))) + 'px'
}
// 初始化
setRem()
window.onresize = function () {
setRem()
}
实现长按监测
html部分
<div id="guide-button">
<!--隐藏的按钮-->
<template v-if="showButtonList">
<div id="old-exhibition">
<img class="exbutton"
:class="[btnIndex == 2?'hit':'']"
src="@/assets/images/exbutton.png"/>
<div class="button-text black-text">
<a :href="jumpUrl[2]"
:class="[btnIndex == 2?'hit':'']">
{{titles[2]}}</a></div></div>
<div id="cloud-exhibition">
<img class="exbutton"
:class="[btnIndex == 1?'hit':'']"
src="@/assets/images/exbutton.png"/>
<div class="button-text black-text">
<a :href="jumpUrl[1]"
:class="[btnIndex == 1?'hit':'']">
{{titles[1]}}</a></div></div>
</template>
<template v-else>
<div id='front'><img src='@/assets/images/front.png'/></div>
<div id="back"><img src='@/assets/images/back.png'/></div>
</template>
<div id="mainbutton">
<img class="exbutton"
@touchstart='goStart'
@touchmove='goMove'
@touchend='goEnd'
src="@/assets/images/mainbutton.png"/>
<div class="button-text">
<a :href="jumpUrl[0]">{{titles[0]}}</a></div>
<img :class="[showButtonList?'':'arrowDown','arrow']"
src="@/assets/images/arrow.png"
@click.self="buttonList"></div>
</div>
JS部分
data(){
return{
showButtonList:false, //是否显示隐藏按钮
timeOutEvent: 0, // 长按事件定时器
startPageY:0, //长按第一个按钮时的起始y坐标
currentPageY:0, //长按移动时的当前y坐标
btnHeight:40, //按钮的长度
titles:['百度','掘金','36氪'],
jumpUrl:['https://www.baidu.com',
'https://juejin.cn/',
'https://www.36kr.com/'],
}
},
computed:{
btnIndex:function(){
//根据位移和按钮长度计算出的触点在哪个按钮上面
let index;
let relativeY = this.startPageY - this.currentPageY;
index = Math.floor(relativeY / this.btnHeight);
console.log(index);
return index;
},
},
methods:{
pageJump(url){
//跳转函数
window.location.href = url;
console.log('jump');
},
buttonList(){
//控制按钮隐藏或显示
this.showButtonList = !this.showButtonList;
console.log('buttonlist',this.showButtonList);
},
goStart(event) {
//监听touchstart事件
let _this = this;
event.preventDefault(); //移动端长按一般会触发“复制”操作,应避免
clearTimeout(_this.timeOutEvent);
let touch = event.targetTouches[0];
console.log('起点',touch.pageY);
this.startPageY = touch.pageY;
// 开始触摸
_this.timeOutEvent = setTimeout(() => {
_this.timeOutEvent = 0
console.log('处理长按事件');
this.showButtonList = true;
},1500); //如果触碰时间超过1.5秒则触发长按事件的响应
},
goMove(event) {
event.preventDefault();
let touch = event.targetTouches[0];
console.log('移动中',touch.pageY);
this.currentPageY = touch.pageY;
},
goEnd(){
let _this = this;
clearTimeout(this.timeOutEvent)
if(_this.timeOutEvent !== 0){
console.log('处理单击事件');
// this.pageJump(this.jumpUrl[0]);
}else{
console.log('处理长按结束事件');
if(this.btnIndex < this.titles.length
&& this.btnIndex >= 0){
this.pageJump(this.jumpUrl[this.btnIndex]);
}
this.showButtonList = false;
this.startPageY = this.currentPageY = 0;
//因为computed会缓存btnIndex,而需要在切换页面之后恢复原状,所以要改变
}
}
},
最后,因为是要实现自适应的,按钮的高度会变化,所以需要实时更新btnHeight。btnHeight未必是准确的按钮高度,还需考虑按钮的间隔等,只要能保证触点经过时能正确触发特效和交互即可,除数1.7是根据实际情况确定的,基本上是按照设计稿比例。
beforeUpdate(){
this.btnHeight = document.getElementById('mainbutton').clientHeight / 1.7;
console.log('btnheight',this.btnHeight);
},