js缓慢动画函数封装

js缓慢动画函数封装

​ 学习pink老师的webAPIs课程过程中的一个小节的过程,现做下笔记并总结一下自己的思考内容。最后再做一点小小的个人拓展。

需求:

  • 封装一个函数 animateX(obj, target, callback),它能实现元素对象进行水平运动的动画效果。
  • 形参 obj 接收进行移动的元素对象,target 接收元素对象目标位置,callback 是元素到达目标位置后执行的回调函数。
  • 给两个按钮分别注册使盒子到达两个目标位置的事件来验证函数的功能。
  • 元素在移动时的速度会越来越慢(缓动动画)。
  • 元素对象可以在两个或多个目标位置间来回运动,且在没有到达上个目标位置之前如果触发到另一个目标的事件则元素可以在中间转向到达另一个位置。

实现原理

1. 动画实现原理
  • 核心原理:通过定时器 setInterval() 不断移动盒子的位置。
  • 实现步骤:
    • 获得盒子的当前位置(obj.offsetLeft)。
    • 让盒子在当前位置加上1个距离(设置定位盒子的 style.left 值)。
    • 利用定时器不断重复这个移动操作。
    • 设置一个结束定时器的条件,已经到达目标位置或者再次触发动画函数。(再次触发时不清除上一个定时器会使多个定时器同时存在,会造成的 bug 下面再说)。
2. 实现缓动效果的原理
  • 核心原理:运动时元素与目标位置的距离在减小,通过将该距离除以10的值作为此次元素运动的距离而达到不断减小步长(移动速度)的目的。
  • 实现步骤:
    • 触发定时器则重新获取当前元素所在的位置。
    • 用目标位置减去当前位置即可得到剩下的运动总路程。
    • 核心算法:(目标值 - 当前位置)/ 10 作为步长( step )。
    • step 要取整,不然到达不了目标位置,会出现小数问题。
3. 取整能让盒子最终到达目标位置的原理
  • 如果如果步长为整数则向上取整。
  • 如果步长为负数则向下取整。
  • 原理:
    • 假设元素最后到距离目标位置10px~20px 的位置,每次向上取整后步长为 2px ,在小于等于 10px 后步长会变为 1px ,最后只剩 1px 步长1px /10 向上取整为 1px 。
    • 负数向下取整的原理同正数。

最终实现代码和结果

代码
<div>
    <button class="btn1">点击到第一个位置100</button>
    <button class="btn2">点击到第二个位置300</button>
    <span></span>
</div>
<script>
    function animateX(obj, target, callback) {
     
     
        // 先清除以前的定时器,只保留当前的一个定时器执行
        clearInterval(obj.timer);
        obj.timer = setInterval(function() {
     
     
            // 步长值写到定时器的里面
            // 把我们步长值改为整数 不要出现小数的问题
            var step = (target - obj.offsetLeft) / 10;
            step = step > 0 ? Math.ceil(step) : Math.floor(step);
            if (obj.offsetLeft == target) {
     
     
                // 停止动画 本质是停止定时器
                clearInterval(obj.timer);
                // 如果传入回调函数则执行,如果没有则不执行
                callback && callback()
            }
            // 把每次加固定值的步长值改为一个慢慢变小的值  步长公式:(目标值 - 现在的位置) / 10
            obj.style.left = obj.offsetLeft + step + 'px';

        }, 15);
    }
    var span = document.querySelector('span');
    var btn1 = document.querySelector('.btn1');
    var btn2 = document.querySelector('.btn2');

    btn1.addEventListener('click', function() {
     
     
        animateX(span, 30);
    })
    btn2.addEventListener('click', function() {
     
     
        animateX(span, 300, function() {
     
     
            span.style.backgroundColor = 'red';
        });
    })
    // 匀速动画 就是 盒子是当前的位置 +  固定的值 10 
    // 缓动动画就是  盒子当前的位置 + 变化的值(目标值 - 现在的位置) / 10)
</script>

结果

注意我在按下按钮2时在元素还没到达300的位置(到达时会变成红色)我又点了按钮1,元素是直接转向回到100的位置,所以满足需求。

细节处理

1. 清除定时器
  • 在元素到达目标位置后清除定时器,防止在到达后虽然元素不会再运动( step为0 ),但是计时器还在运行,占用内存资源。
  • 在定时器声明前先清除定时器。这是为了防止我们在上个点击事件还在运行,即上个动画函数里面的定时器还在运行时,我们又点击一次按钮或者点击另一个按钮触发另一个动画函数。这时这个元素对象上会同时存在多个定时器同时运行。影响元素的运动状态。如下图所示。

当上一个向右运动的定时器还没结束时再开一个向左运动的定时器,出现上图情况。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mE26qa4z-1611173022840)(https://s3.ax1x.com/2021/01/11/sG3yCQ.gif)]

不断点同一个方向的定时器会使动画速度加快。

2. 定时器的声明问题
  • 在声明定时器时老师还没有讲到在运行声明定时器前先清除定时器,所以老师将使用 var timer = setInterval()声明定时器的方式改成将定时器添加为 obj 对象的属性的理由和想法是如果多个元素都使用这个动画函数,每次都要var 声明定时器(占用内存)。我们可以给不同的元素使用不同的定时器(自己专门用自己的定时器,这样也可以方便区分。)。 核心原理:利用 JS 是一门动态语言,可以很方便的给当前对象添加属性。
  • 但是我在看代码时还想到了一点,就是在我们触发点击事件时就会产生一个函数作用域,而如果使用 var 声明定时器,这个 timer 定时器变量是个局部变量。那么上面那句清除定时器的代码实际是相当于没有的。即在另一个动画函数内无法清除其他动画函数里面的定时器。因此我们更需要使用上述方式添加定时器。

自己的一些小尝试

在这里插入图片描述

我自己给这个动画函数添加了可以控制速度的功能,分为三档。快速,中等速度,慢速。当然过程中它还是会以缓慢动画的速度特点移动(没有改变步长递减的特性)。还添加了一个过程计时的效果,为此不能让元素在还没到达目标位置时出现转向(因为我计时不知改怎么改好了,哈哈)。所以所以用了老师后面讲到的添加节流阀的方法。有个瑕疵就是在元素运动时那三个调速按钮还能按,以后有空再改了。

<script>
    var btn1 = document.querySelector('.btn1');
    var btn2 = document.querySelector('.btn2');
    var div1 = document.querySelector('.div1');
    var timeCount = document.querySelector('.time_count'); // 计时的span盒子
    var speed = 0; // 默认速度中等
    var span = document.querySelector('.speed')
    var spans = span.querySelectorAll('span'); // 获取三个速度按键元素

    function animateX(obj, target, timingFunction, callback) {
     
      // speed 可以传入的参数自定义有fast slow medium(默认)
        var init = obj.offsetLeft; // 获得该元素对象初始的位置
        var distance = target - init; // 获得移动的距离
        var stepLength = distance / 10; // 计算步长
        var starTime = +new Date(); // 开始的时间
        var flag = 1;
        var counts = 0;
        if (distance != 0) {
     
     
            var t = setInterval(function(f) {
     
      /* 第一次将flag以参数形式传进计时函数发现里面的定时器会在执行完一次后就清除了即计时只停在100ms */
                f = flag;
                if (!f) {
     
     
                    clearInterval(t);
                }
                var nowTime = +new Date();
                counts = nowTime - starTime;
                timeCount.innerHTML = counts + 'ms';
            }, 200);
        }


        if (timingFunction == 2) {
     
     
            time = 180;
        } else if (timingFunction == 0) {
     
     
            time = 60;
        } else {
     
     
            time = 90;
        } // 处理速度的问题
        // clearInterval(obj.timer); 此时也不需要清除定时器了,因为有函数锁只需要在下面清除即可
        // timesCount(starTime, flag);
        obj.timer = setInterval(function() {
     
     
            if (distance == 0) {
     
      // 移动距离为0时移除定时器并执行回调函数
                window.clearInterval(obj.timer);
                callback && callback();
                flag = 0;
            }
            // 如果移动距离不为0则进行移动
            // 处理不能精确到达目标位置和当目标位置在左边时的情况
            stepLength = stepLength > 0 ? Math.ceil(stepLength) : Math.floor(stepLength);
            obj.style.left = init + stepLength + 'px'; // 使定位的盒子的left值发生改变实现移动效果
            init = obj.offsetLeft; // 重新获取对象的初始位置和还需要移动的距离,为计时器下次执行作准备。
            distance = target - init;
            stepLength = distance / 10;
        }, time);
    }
    // 给三个设置速度的按钮注册事件
    for (var i = 0; i < spans.length; i++) {
     
     
        // 使用立即执行函数
        (function(j) {
     
     
            spans[j].setAttribute('data-speed', j);

        })(i);
        spans[i].addEventListener('click', function() {
     
     
            for (var j = 0; j < spans.length; j++) {
     
     
                spans[j].className = '';
            }
            this.className = 'current';
            speed = this.getAttribute('data-speed');
        })
    }
    // 定义一个计时函数,参数是开始的时间,因为只有动画开始时才计时所以只能在开始时再传参

    var lock = 1; // 设置函数锁防止快速点击出现计时问题
    btn1.onclick = function() {
     
     
        if (lock) {
     
     
            lock = 0;
            animateX(div1, 500, speed, function() {
     
     
                div1.style.backgroundColor = 'black';
                lock = 1;
            })
        }


    }
    btn2.onclick = function() {
     
     
        if (lock) {
     
     
            lock = 0;
            animateX(div1, 0, speed, function() {
     
     
                div1.style.backgroundColor = 'green';
                lock = 1;
            })
        }

    }
</script>

猜你喜欢

转载自blog.csdn.net/TKOP_/article/details/112503938
今日推荐