JavaScript動畫詳解(一) —— 循環與事件監聽

吳青強 8年前發布 | 49K 次閱讀 JavaScript開發 JavaScript

其實Web動畫的實現原理跟早期的運動影片很類似,都是通過將一張張的賽璐珞片以較快速度播放,從而模擬出連貫的物體運動。而這一張張的賽璐珞片就類似于投影運動媒體的幀的概念,而幾乎所有投影運動媒體都是通過幀來實現的。

動畫循環

幾乎所有的程序動畫都會表現為某種形式的循環,我們會創建一個展現一系列圖像的流程圖以實現逐幀動畫,其中每一幀只需要繪制出來即可。

為了實現動畫,需要為每一幀執行以下操作:

1 . 執行該幀所要調用到的代碼;

2 . 將所有對象繪制到出來;

3 . 重復這一過程渲染下一幀。

動畫循環函數

在H5時代,實現Web動畫的方法有很多:

可以使用CSS3中的animation + keyframes或者transition,可以通過SVG中的SMIL動畫接口,也可以借助jQuery動畫相關的API。還可以使用 JavaScript中最原始window.setTimout()和window.setInterval()通過不斷更新元素的狀態位置等來實現動 畫。

在HTML5中又提出了一種新的基于瀏覽器優化動畫實現的方案 —— window.requestAnimationFrame()方法。

下面主要討論一下JavaScript中動畫循環函數 —— setTimeout()、setInterval()和requestAnimationFrame(),他們的對應取消循環函數分別是 clearTimeout()、clearInterval()和cancelAnimationFrame()。

setTimeout()

setTimeout實現循環動畫的原理:

(function drawFrame() {
    var timer = null;
    var delayTime = 1000 / 60;
    // 幀渲染和幀繪制 ...
    timer = setTimeout(drawFrame, delayTime);
    // 停止循環
    if( /* 停止條件成立 */ ) {
        clearTimeout(timer);
    }
})();

setInterval()

setInterval實現循環動畫的原理:

var timer = null;
var delayTime = 1000/ 60;
timer = setInterval(drawFrame, delayTime);

function drawFrame() {
    // 幀渲染和幀繪制 ...
    // 停止循環
    if( /* 動畫停止條件成立 */ ) {
        clearInterval(timer);
    }
}

setInterval卻沒有被所調用的函數所束縛,它只是簡單地每隔一定時間就重復執行一次所調用的函數。而setTimeout受所調用函數的影響,只有執行完成該次的函數調用,才能繼續執行下一次的函數調用。

如果要求在每隔一個固定的時間間隔后就精確地執行某動作,那么最好使用setInterval。

如果不想由于連續調用產生互相干擾的問題,尤其是每次函數的調用需要繁重的計算以及很長的處理時間,那么最好使用setTimeout。

requestAnimationFrame()

requestAnimationFrame()的原理其實與setTimeout和setInterval類似,通過遞歸調用同一方法來不斷更新 畫面以達到動畫效果,但它優于setTimeout和setInterval的地方在于它是由瀏覽器專門為動畫提供優化實現的API,并且充分利用顯示器 的刷新機制,比較節省系統資源。顯示器有固定的刷新頻率(60Hz或75Hz),也就是說,每秒最多只能重繪60次或75 次,requestAnimationFrame的基本思想就是與這個刷新頻率保持同步,利用這個刷新頻率進行頁面重繪。此外,使用這個API,一旦頁面 不處于瀏覽器的當前標簽,就會自動停止刷新。這就節省了CPU、GPU和電力。

不過有一點需要注意,requestAnimationFrame是在主線程上完成。這意味著,如果主線程非常繁忙,requestAnimationFrame的動畫效果會大打折扣。

requestAnimationFrame的語法如下:

requestAnimationFrame(callback) //callback為回調函數

requestAnimationFrame動畫的實現原理與setTimeout類似,都是使用一個回調函數作為參數,且這個回調函數會在瀏覽器重繪之前調用。具體如下:

(function drawFrame() {
    var timer = null;
    // 幀渲染和幀繪制 ...
    timer = requestAnimationFrame(drawFrame);
    // 停止循環
    if( /* 停止條件成立 */ ) {
        cancelAnimationFrame(timer);
    }
})();

由于requestAnimationFrame是HTML5新定義的API,舊版本的瀏覽器并不兼容,而且瀏覽器的實現方式不一,此時就需要考慮到兼容性問題了。常用的兼容性寫法如下:

window.requestAnimFrame = (function(){
      return  window.requestAnimationFrame       || 
              window.webkitRequestAnimationFrame || 
              window.mozRequestAnimationFrame    || 
              window.oRequestAnimationFrame      || 
              window.msRequestAnimationFrame     || 
              function( callback ){
                window.setTimeout(callback, 1000 / 60);
              };
})();

window.cancelAnimationFrame = (function () {
    return window.cancelAnimationFrame ||
            window.webkitCancelAnimationFrame ||
            window.mozCancelAnimationFrame ||
            window.oCancelAnimationFrame ||
            function (timer) {
                window.clearTimeout(timer);
            };
})();

上面兼容性代碼作用有兩個,一是把各瀏覽器前綴進行統一,二是在瀏覽器沒有requestAnimationFrame方法時將其指向setTimeout方法。

更具體的兼容性請看這里~http://caniuse.com/#feat=requestanimationframe

截個圖,如下:

JavaScript動畫詳解(一) —— 循環與事件監聽

事件監聽

在動畫中我們也許常用到用戶交互效果,而用戶交互是基于用戶事件的,這些事件通常是鼠標事件、觸摸事件以及鍵盤事件。

事件監聽器

事件監聽器是監聽事件的對象。最原始的時間監聽是使用"on + 事件"的方法監聽事件,如"onclick"等。在現在標準Web瀏覽器中還可以通過調用DOM元素的addEventListener()方法指定它作 為某個特定時間的監聽器,在IE6~8中不兼容addEventListener(),但可以使用attachEvent()來實現類似的效果。

常用的兼容性寫法如下:

// eventType為不含"on"的事件類型
var bind = (function(ele, eventType, callback) {
    if(ele.addEventListener) {
        // W3C標準寫法
        return ele.addEventListener(eventType, callback, false);
    }else if(ele.attachEvent) {
        // 兼容IE6~8
        return ele.attachEvent(eventType, callback);
    }else {
        // 兼容IE5-
        return ele["on" + eventType] = callback;
    }
})();

var unbind = (function(ele, eventType, callback) {
    if(ele.removeEventListener) {
        // W3C標準寫法
        return ele.removeEventListener(eventType, callback, false);
    }else if(ele.detachEvent) {
        // 兼容IE6~8
        return ele.detachEvent(eventType, callback);
    }else {
        // 兼容IE5-
        return ele["on" + eventType] = null;
    }
})();

常見的事件類型

鼠標事件:

onmousedown, onmouseup, onclick, ondbclick, onmousewheel, onmousemove, onmouseover, onmouseout;

觸摸事件:

ontouchstart, ontouchend, ontouchmove;

鍵盤事件:

onkeydown, onkeyup, onkeypress;

頁面相關事件:

onabort(圖片在下載時被用戶中斷), onbeforeunload(當前頁面的內容將要被改變時觸發), onerror(出現錯誤時觸發), onload(內容加載完成時觸發), onmove(瀏覽器窗口被移動時觸發), onresize(瀏覽器的窗口大小被改變時觸發), onscroll(滾動條位置發生變化時觸發), onstop(瀏覽器的停止按鈕被按下時觸發此事件或者正在下載的文件被中斷時觸發), onunload(當前頁面將被改變時觸發);

表單相關事件

onblur(元素失去焦點時觸發), onchange(元素失去焦點且元素內容發生改變時觸發), onfocus(元素獲得焦點時觸發), onreset(表單中reset屬性被激活時觸發), onsubmit(表單被提交時觸發);oninput(在input元素內容修改后立即被觸發,兼容IE9+)

編輯事件

onbeforecopy:當頁面當前的被選擇內容將要復制到瀏覽者系統的剪貼板前觸發此事件;

onbeforecut:當頁面中的一部分或者全部的內容將被移離當前頁面[剪貼]并移動到瀏覽者的系統剪貼板時觸發此事件;

onbeforeeditfocus:當前元素將要進入編輯狀態;

onbeforepaste:內容將要從瀏覽者的系統剪貼板傳送[粘貼]到頁面中時觸發此事件;

onbeforeupdate:當瀏覽者粘貼系統剪貼板中的內容時通知目標對象;

oncontextmenu:當瀏覽者按下鼠標右鍵出現菜單時或者通過鍵盤的按鍵觸發頁面菜單時觸發的事件;

oncopy:當頁面當前的被選擇內容被復制后觸發此事件;

oncut:當頁面當前的被選擇內容被剪切時觸發此事件;

onlosecapture:當元素失去鼠標移動所形成的選擇焦點時觸發此事件;

onpaste:當內容被粘貼時觸發此事件;

onselect:當文本內容被選擇時的事件;

onselectstart:當文本內容選擇將開始發生時觸發的事件;

拖動事件

ondrag:當某個對象被拖動時觸發此事件 [活動事件];

ondragdrop:一個外部對象被鼠標拖進當前窗口時觸發;

ondragend:當鼠標拖動結束時觸發此事件;

ondragenter:當對象被鼠標拖動的對象進入其容器范圍內時觸發此事件;

ondragleave:當對象被鼠標拖動的對象離開其容器范圍內時觸發此事件;

ondragover:當某被拖動的對象在另一對象容器范圍內拖動時觸發此事件;

ondragstart:當某對象將被拖動時觸發此事件;

ondrop:在一個拖動過程中,釋放鼠標鍵時觸發此事件;

事件的常見應用

獲取鼠標位置

每個鼠標事件都有兩個屬性用于確定鼠標當前位置:pageX和pageY。但是IE6~8不知持這兩個屬性,需要用到clientX和clientY。

其中,pageX和pageY的鼠標位置是相對于document文檔的,而clientX和clientY的鼠標位置是相對于瀏覽器屏幕的。為了實現各平臺統一,兼容性寫法可以如下:

// 初始化鼠標位置,這里的鼠標位置默認是相對于document文檔的
var mouse = {
    x: 0,
    y: 0
}; 
function getMouse(event) {
    var event = event || window.event;
    if(event.pageX || event.pageY) {
        x = event.x;
        y = event.y;
    }else {
        var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
        var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
        x = event.clientX + scrollLeft;
        y = event.clientY + scrollTop;
    }
    mouse.x = x;
    mouse.y = y;

    return mouse;
}

觸摸位置

一個觸摸點可以被想象成鼠標光標,不過鼠標光標會一直停留在屏幕上,而手指卻會從設備上按下、移動以及釋放,所以某些時刻光標會從屏幕上消失。另 外,觸摸屏上不存在mouseover等效的觸摸事件。同一時間可能發生多點觸摸,某個觸摸點的信息會保存在觸摸事件的一個數組中。

獲取觸摸位置的方法見下:

// 觸摸位置聲明
var touch = {
    x: null,
    y: null,
    isPress: false
}

function getTouch (event) {
    var x, y, 
    touchEvent = event.touches[0]; //獲取觸摸位置的第一個觸摸點
    var event = event || window.event;
    if(touchEvent.pageX || touchEvent.pageY) {
        touchEvent.pageX;
        y = touchEvent.pageY;
    }else {
        var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
        var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
        x = touchEvent.clientX + scrollLeft;
        y = touchEvent.clientY + scrollTop;
    }
    touch.x = x;
    touch.y = y;

    return touch;
}

常用的方法是,如果不存在有效的觸摸點是,x和y的值應設置為null。

element.addEventListener("touchstart", function(event) {
    touch.isPressed = true;
}, false);
element.addEventListener("touchsend", function(event) {
    touch.isPressed = false;
    touch.x = null;
    touch.y = null;
}, false);
element.addEventListener("touchsend", function(event) {
    if(touch.isPressed) {
        getTouch (event);
    }
}, false);

獲取鍵盤碼

獲得鍵盤碼可以使用event.keyCode。具體實現如下:

var keyCode;
function getKeyCode(event) {
    var event = event || window.event;
    keyCode = event.keyCode;
    return keyCode;
}

鼠標滾輪事件的判定

見我寫的另外一篇文章《JavaScript滾輪事件兼容性寫法

總結

多數Web動畫都是由一幀幀的狀態通過較快速度的播放模擬出來的,所以循環計時函數在這里就起到了連接連貫的幀狀態的作用。而動畫更多時候需要用戶交互,所以事件和事件監聽尤顯重要。最后我列出了幾個經常用到幾個與事件相關的封裝應用,方便自己查閱和調用。

下一篇,我想寫點關于緩動動畫的。

來自:http://www.dengzhr.com/js/447

Save

 本文由用戶 吳青強 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!