在移动端开发中为了防止点击穿透行为,我们有时会阻止浏览器的默认行为,此时我们需要自定义所需浏览器行为,本文就介绍下自定义浏览器的滚动条。

移动端浏览器的默认行为

浏览器的m默认行为主要有以下几点:

  • 上下滑动 滚动条
  • 文字、图片长按选中
  • 输入框获取焦点
  • 用户缩放
  • 模拟click动作(点击穿透)
  • 下滑的空白区域
  • ...

Tween算法

由于常规的css过渡和定时难以实现滚动条的即点即停效果,此时我们需引入tween算法来实现相应的效果。

参数说明:
t: current time:当前时间;
b: beginning value:初始值,元素的初始位置;
c: change in value:变化量,元素的结束位置与初始位置距离差
d: duration:持续时间,整个过渡持续时间
s:Elastic和Back 的可选参数。回弹系数,s值越大.回弹效果越远。
​
返回值:
元素每一次运动到的位置
/*tween类*/
var Tween = {
    Linear: function(t,b,c,d){ return c*t/d + b; },
    Quad: {
        easeIn: function(t,b,c,d){
            return c*(t/=d)*t + b;
        },
        easeOut: function(t,b,c,d){
            return -c *(t/=d)*(t-2) + b;
        },
        easeInOut: function(t,b,c,d){
            if ((t/=d/2) < 1) return c/2*t*t + b;
            return -c/2 * ((--t)*(t-2) - 1) + b;
        }
    },
    Cubic: {
        easeIn: function(t,b,c,d){
            return c*(t/=d)*t*t + b;
        },
        easeOut: function(t,b,c,d){
            return c*((t=t/d-1)*t*t + 1) + b;
        },
        easeInOut: function(t,b,c,d){
            if ((t/=d/2) < 1) return c/2*t*t*t + b;
            return c/2*((t-=2)*t*t + 2) + b;
        }
    },
    Quart: {
        easeIn: function(t,b,c,d){
            return c*(t/=d)*t*t*t + b;
        },
        easeOut: function(t,b,c,d){
            return -c * ((t=t/d-1)*t*t*t - 1) + b;
        },
        easeInOut: function(t,b,c,d){
            if ((t/=d/2) < 1) return c/2*t*t*t*t + b;
            return -c/2 * ((t-=2)*t*t*t - 2) + b;
        }
    },
    Quint: {
        easeIn: function(t,b,c,d){
            return c*(t/=d)*t*t*t*t + b;
        },
        easeOut: function(t,b,c,d){
            return c*((t=t/d-1)*t*t*t*t + 1) + b;
        },
        easeInOut: function(t,b,c,d){
            if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b;
            return c/2*((t-=2)*t*t*t*t + 2) + b;
        }
    },
    Sine: {
        easeIn: function(t,b,c,d){
            return -c * Math.cos(t/d * (Math.PI/2)) + c + b;
        },
        easeOut: function(t,b,c,d){
            return c * Math.sin(t/d * (Math.PI/2)) + b;
        },
        easeInOut: function(t,b,c,d){
            return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b;
        }
    },
    Expo: {
        easeIn: function(t,b,c,d){
            return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b;
        },
        easeOut: function(t,b,c,d){
            return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
        },
        easeInOut: function(t,b,c,d){
            if (t==0) return b;
            if (t==d) return b+c;
            if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b;
            return c/2 * (-Math.pow(2, -10 * --t) + 2) + b;
        }
    },
    Circ: {
        easeIn: function(t,b,c,d){
            return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b;
        },
        easeOut: function(t,b,c,d){
            return c * Math.sqrt(1 - (t=t/d-1)*t) + b;
        },
        easeInOut: function(t,b,c,d){
            if ((t/=d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b;
            return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b;
        }
    },
    Elastic: {
        easeIn: function(t,b,c,d,a,p){
            if (t==0) return b;  if ((t/=d)==1) return b+c;  if (!p) p=d*.3;
            if (!a || a < Math.abs(c)) { a=c; var s=p/4; }
            else var s = p/(2*Math.PI) * Math.asin (c/a);
            return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
        },
        easeOut: function(t,b,c,d,a,p){
            if (t==0) return b;  if ((t/=d)==1) return b+c;  if (!p) p=d*.3;
            if (!a || a < Math.abs(c)) { a=c; var s=p/4; }
            else var s = p/(2*Math.PI) * Math.asin (c/a);
            return (a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b);
        },
        easeInOut: function(t,b,c,d,a,p){
            if (t==0) return b;  if ((t/=d/2)==2) return b+c;  if (!p) p=d*(.3*1.5);
            if (!a || a < Math.abs(c)) { a=c; var s=p/4; }
            else var s = p/(2*Math.PI) * Math.asin (c/a);
            if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
            return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b;
        }
    },
    Back: {
        easeIn: function(t,b,c,d,s){
            if (s == undefined) s = 1.70158;
            return c*(t/=d)*t*((s+1)*t - s) + b;
        },
        easeOut: function(t,b,c,d,s){
            if (s == undefined) s = 1.70158;
            return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
        },
        easeInOut: function(t,b,c,d,s){
            if (s == undefined) s = 1.70158; 
            if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b;
            return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
        }
    },
    Bounce: {
        easeIn: function(t,b,c,d){
            return c - Tween.Bounce.easeOut(d-t, 0, c, d) + b;
        },
        easeOut: function(t,b,c,d){
            if ((t/=d) < (1/2.75)) {
                return c*(7.5625*t*t) + b;
            } else if (t < (2/2.75)) {
                return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b;
            } else if (t < (2.5/2.75)) {
                return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b;
            } else {
                return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b;
            }
        },
        easeInOut: function(t,b,c,d){
            if (t < d/2) return Tween.Bounce.easeIn(t*2, 0, c, d) * .5 + b;
            else return Tween.Bounce.easeOut(t*2-d, 0, c, d) * .5 + c*.5 + b;
        }
    }
}

使用案例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        #outer {
            margin: 50px;
            width: 600px;
            height: 40px;
            border: 5px solid green;
        }
        #inner {
            width: 0;
            height: 40px;
            background: #f90;
        }
        .btn{
            margin-top: 20px;
        }
    </style>
</head>
<body>

    <div id="outer">
        <div id="inner"></div>
        <div class="btn">
            <button class="btn1">匀速</button>
            <button class="btn2">减速</button>
            <button class="btn3">回弹</button>
        </div>
    </div>
    <script>
        //定义Tween算法的函数
        var tween = {
            //匀速
            linear: function(t,b,c,d){ return c*t/d + b; },
            //减速
            cubicEaseOut: function(t,b,c,d){
                return c*((t=t/d-1)*t*t + 1) + b;
            },
            //回弹
            backEaseOut: function(t,b,c,d,s){
                if (s === undefined) s = 1.70158;
                return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
            },
        };

        /*
        *  Tween 函数的参数
        *    t   current time  当前时间
        *    b   beginning value 初始值
        *    c   change in value  值的变量量(目标值-初始值)
        *    d   duration    持续时间
        *    s   回弹系数
        * */

        var inner = document.querySelector('#inner');
        var btn1 = document.querySelector('.btn1');
        var btn2 = document.querySelector('.btn2');
        var btn3 = document.querySelector('.btn3');



        btn1.onclick = function () {
            var t = 0;
            var b = 0;
            var c = 600;
            var d = 5000;
            var intervalId = setInterval(function(){
                t += 10;
                var value = tween.linear(t, b, c, d); //匀速
                inner.style.width = value + 'px';
                // 如果当前的时间点,已经到达了持续总时间 听了
                if (t >= d) {
                    clearInterval(intervalId);
                }
            }, 10);
        };
        btn2.onclick = function () {
            var t = 0;
            var b = 0;
            var c = 600;
            var d = 5000;
            var intervalId = setInterval(function(){
                t += 10;
                var value = tween.cubicEaseOut(t, b, c, d); //减速
                inner.style.width = value + 'px';
                // 如果当前的时间点,已经到达了持续总时间 听了
                if (t >= d) {
                    clearInterval(intervalId);
                }
            }, 10);
        };
        btn3.onclick = function () {
            var t = 0;
            var b = 0;
            var c = 600;
            var d = 5000;
            var intervalId = setInterval(function(){
                t += 10;
                var value = tween.backEaseOut(t, b, c, d); //回弹
                inner.style.width = value + 'px';
                // 如果当前的时间点,已经到达了持续总时间 听了
                if (t >= d) {
                    clearInterval(intervalId);
                }
            }, 10);
        };

    </script>
</body>
</html>

自定义屏幕滚动条

//自定义屏幕滚动插件 touchScroll.js
(function(w){
    /**
     * 实现屏幕滚动
     * @param wrapper   包裹元素
     * @param content   内容元素(负责位置变化)
     * @param scrollBar 滚动条元素;可选;如果不指定,就没有滚动条
     */
    function touchScroll(wrapper, content, scrollBar){
        var intervalId = null;  //定义定时器标记

        //计算滚动条的高度
        if (scrollBar) {
            var scale1 = wrapper.clientHeight / content.offsetHeight; //比例
            scrollBar.style.height = wrapper.clientHeight * scale1 + 'px';
        }

        //开启3d加速
        transformCss(content, 'translateZ', 0);

        //触摸开始
        wrapper.addEventListener('touchstart', function(event){
            //获取触点对象
            var touch = event.targetTouches[0];
            // 获取触点的起始位置
            this.startY = touch.clientY;
            // 获取 content 的起始位置
            this.eleY = transformCss(content, 'translateY');
            // 初始化 触点滑动距离
            this.dstY = 0;
            //初始时间
            this.startTime = Date.now();

            //滚动条显示
            if (scrollBar) {
                scrollBar.style.opacity = 1;
            }
            //取消定时
            clearInterval(intervalId);
        });
        //触摸移动
        wrapper.addEventListener('touchmove', function(event){
            //获取触点对象
            var touch = event.targetTouches[0];
            //获取触点的结束位置
            var endY = touch.clientY;
            //计算触点的滑动距离
            this.dstY = endY - this.startY;

            // 根据滑动距离 计算 content 的位置
            var translateY = this.eleY + this.dstY;

            //判断临界值,开启橡皮筋效果
            if (translateY >= 0) { //上边界
                //计算比例
                var scale = 1 - translateY / (wrapper.clientHeight * 1.9);
                //重新计算translateY
                translateY *= scale;
            } else if (translateY <= (wrapper.clientHeight - content.offsetHeight)) {
                //计算content距离视口底部距离
                var bottomY =  wrapper.clientHeight - (content.offsetHeight + translateY);
                // 计算比例
                var scale = 1 - bottomY / (wrapper.clientHeight * 1.9);
                // 重新计算 bottomY
                bottomY *= scale;
                // 重新计算 translateY
                translateY = (wrapper.clientHeight - bottomY) - content.offsetHeight
            }

            // 设置 content 的位置
            transformCss(content, 'translateY', translateY);

            // 调整滚动条位置
            if (scrollBar) {
                setScrllBarOffset(translateY);
            }
        });

        //触摸结束
        wrapper.addEventListener('touchend', function(event){
            //结束时间,计算时间差
            var dstTime = Date.now() - this.startTime;
            //计算加速距离
            var speed = this.dstY / dstTime * 200;

            // 计算此时元素的位置 并且加上 加速距离
            var translateY = transformCss(content, 'translateY');
            translateY += speed;

            //设置过渡类型
            var type = 'cubicEaseOut';  //一直在减速

            //判断到达临界点,开启回弹
            if (translateY >= 0) {
                translateY = 0;
                type = 'backEaseOut';
            } else if (translateY <= wrapper.clientHeight - content.offsetHeight) {
                translateY = wrapper.clientHeight - content.offsetHeight;
                type = 'backEaseOut';
            }

            //重新设置 content 位置 调用过渡函数
            moveTo(content, translateY, 500, type);
        });

        /**
         * 过渡函数
         * @param node  要发生变换的元素
         * @param target    目标值
         * @param duration      过渡持续时间
         * @param type  过渡类型 linear、cubicEaseOut、backEaseOut
         */
        function moveTo(node, target, duration, type = 'linear') {

            //定义Tween算法的函数
            var tween = {
                //匀速
                linear: function(t,b,c,d){ return c*t/d + b; },
                //减速
                cubicEaseOut: function(t,b,c,d){
                    return c*((t=t/d-1)*t*t + 1) + b;
                },
                //回弹
                backEaseOut: function(t,b,c,d,s){
                    if (s === undefined) s = 1.70158;
                    return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
                },
            };

            // 定义Tween函数的参数
            var t = 0;  //当前时间
            var b = transformCss(node, 'translateY');  //起始值
            var c = target - b;  //变化量
            var d = duration;

            //避免多次定时,清除之前的定时(不论之前有没有定时)
            clearInterval(intervalId);
            //开启定时
            intervalId = setInterval(function(){
                //t变换
                t += 10;
                //利用Tween函数获取当前的值
                var translateY = tween[type](t,b,c,d);
                // 设置content 的位置
                transformCss(node, 'translateY', translateY);

                //调整滚动条的位置
                if (scrollBar) {
                    setScrllBarOffset(translateY);
                }

                //判断过渡完毕
                if (t >= d) {
                    //定时结束 过渡结束
                    clearInterval(intervalId);
                    //滚动条隐藏
                    if (scrollBar) {
                        scrollBar.style.opacity = 0;
                    }
                }
            }, 10);
        }

        /**
         * 设置滚动条的位置
         * @param contentTranslateY 内容此时的位置
         */
        function setScrllBarOffset(contentTranslateY) {
            // 计算比例  content当前位置 / 最大位置
            var scale2 = -contentTranslateY / (content.offsetHeight - wrapper.clientHeight);
            // 计算滚动条位置
            transformCss(scrollBar, 'translateY', (wrapper.clientHeight - scrollBar.offsetHeight) * scale2);
        }
    }

    //把方法暴露
    w.touchScroll = touchScroll;
})(window);

使用案例-竖向滑动

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0,viewport-fit:cover">
    <title>自定义竖向滑动</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }
        html,body,#app {
            width:100%;
            height: 100%;
            overflow: hidden;
        }
        #content {
            font-size: 26px;
        }
        #scrollBar {
            position: absolute;
            top: 0;
            right: 0;
            width: 4px;
            height: 100px;
            border-radius: 2px;
            background-color: rgba(0,0,0,.8);
            opacity: 0;
            transition: opacity .2s;
        }
    </style>
</head>
<body>
    <div id="app">
        <div id="content"></div>
        <div id="scrollBar"></div>
    </div>

    <script src="js/transformcss.js"></script>
    <script src="js/touchscroll.js"></script>
    <script>
        (function () {
            var app = document.querySelector('#app');
            var content = document.querySelector('#content');
            var scrollBar = document.querySelector('#scrollBar');

            app.addEventListener('touchstart', function(event){
                event.preventDefault();
            });

            for (var i = 0; i <= 200; i ++) {
                content.innerHTML += i + '<br>';
            }
            touchScroll(app, content, scrollBar);
        })();
    </script>
</body>
</html>

注:为了更方便简洁的实现触摸移动,因此在自定义滚动条(touchScroll.js)时使用了transformcss.js文件,因此在使用自定义滚动条插件时需先引入transformcss.js,地址:https://segmentfault.com/a/11...,或直接查看笔者上一篇文章: 封装transfrom函数。


说明:笔者只是个在前端道路上默默摸索的初学者,若本文涉及错误请及时给予纠正,如果本文对您有帮助的话,点击铭哥哥网址 http://learn.fuming.site/ 学习更多!


捕猹少年闰土
42 声望0 粉丝