1. 程式人生 > 其它 >js緩慢動畫函式封裝

js緩慢動畫函式封裝

技術標籤:javascriptjavascript

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>

sG1I1A.gif

結果

注意我在按下按鈕2時在元素還沒到達300的位置(到達時會變成紅色)我又點了按鈕1,元素是直接轉向回到100的位置,所以滿足需求。

細節處理

1. 清除定時器
  • 在元素到達目標位置後清除定時器,防止在到達後雖然元素不會再運動( step為0 ),但是計時器還在執行,佔用記憶體資源。
  • 在定時器宣告前先清除定時器。這是為了防止我們在上個點選事件還在執行,即上個動畫函式裡面的定時器還在執行時,我們又點選一次按鈕或者點選另一個按鈕觸發另一個動畫函式。這時這個元素物件上會同時存在多個定時器同時執行。影響元素的運動狀態。如下圖所示。

sG3mcR.gif

當上一個向右運動的定時器還沒結束時再開一個向左運動的定時器,出現上圖情況。

sG3yCQ.gif

不斷點同一個方向的定時器會使動畫速度加快。

2. 定時器的宣告問題
  • 在宣告定時器時老師還沒有講到在執行宣告定時器前先清除定時器,所以老師將使用 var timer = setInterval()宣告定時器的方式改成將定時器新增為 obj 物件的屬性的理由和想法是如果多個元素都使用這個動畫函式,每次都要var 宣告定時器(佔用記憶體)。我們可以給不同的元素使用不同的定時器(自己專門用自己的定時器,這樣也可以方便區分。)。 核心原理:利用 JS 是一門動態語言,可以很方便的給當前物件新增屬性。
  • 但是我在看程式碼時還想到了一點,就是在我們觸發點選事件時就會產生一個函式作用域,而如果使用 var 宣告定時器,這個 timer 定時器變數是個區域性變數。那麼上面那句清除定時器的程式碼實際是相當於沒有的。即在另一個動畫函式內無法清除其他動畫函式裡面的定時器。因此我們更需要使用上述方式新增定時器。

自己的一些小嚐試

sG8krt.gif

我自己給這個動畫函式添加了可以控制速度的功能,分為三檔。快速,中等速度,慢速。當然過程中它還是會以緩慢動畫的速度特點移動(沒有改變步長遞減的特性)。還添加了一個過程計時的效果,為此不能讓元素在還沒到達目標位置時出現轉向(因為我計時不知改怎麼改好了,哈哈)。所以所以用了老師後面講到的函式鎖的方法。有個瑕疵就是在元素運動時那三個調速按鈕還能按,以後有空再改了。

<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>