移动端按钮长按弹出,长按移动,松开选定

目录

  • 演示
  • 原理
    • touch对象
    • touch事件
    • targetTouches和touches的区别
  • 实现(代码部分)

演示

image
第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区域,打印结果如下:
image
点开changedTouches,可以看到,force属性值改变了,导致点击的触点touch对象被记录下来了。
相应的,当触点位置移动时,changedTouches也会记录。当从layer2移动到layer1范围,记录的还是一开始的那个触点,只不过其属性会随触点移动发生变化:
image
这里是从“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的对比

image
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);
    },
原文地址:https://www.cnblogs.com/liulangbxc/p/15234722.html