移動web:翻頁場景動畫

  在移動web,特別是在微信中,常常看到一種翻頁動畫效果,也稱爲場景動畫。javascript

  一頁一頁的翻過,像在看書,每頁的內容以各類"炫酷"的效果出如今你的眼裏,配上一首動聽的音樂,你有沒有喜歡上呢。css

  這裏沒有音樂,沒有炫酷的出場,只有實實在在的翻頁。html

  先看看效果(若是不能查看 複製下面的代碼保存在本地查看 或者在移動設備查看本頁面java

css3

   先貼上代碼,僅供參考web

/**
 * LBS slidePage 絕對定位方式 支持WP
 * Date: 2014-11-20
 * ===================================================
 * opts.el 外圍包裹容器/滑動事件對象(一個字符串的CSS選擇器或者元素對象)
 * opts.index 索引(默認0) 指定顯示哪一個索引的頁
 * opts.current    當前頁添加的類名(默認'current')
 * opts.navShow 是否須要導航指示 默認false不須要 
 * opts.navClass 導航指示容器的類名 方便設置樣式 (默認'slide-nav') 
 * opts.auto 是否自動播放 默認false
 * opts.delay 自動播放間隔時間 默認5000(單位毫秒) 自動播放時有效
 * opts.locked 是否鎖定頭尾滑動  默認false 若是開啓則不能使用自動播放
 * opts.effect 動畫效果(平移=translate 縮放=scale 重疊=overlap) 默認平移
 * opts.duration 動畫持續時間 默認300(單位毫秒) 
 * opts.minScale 動畫效果爲縮放時的最小縮放比率(0 ~ 1) 1爲沒有縮放效果 默認0.5
 * opts.start 手指按下時 執行函數
 * opts.move 手指移動中 執行函數
 * opts.end 手指收起後 執行函數
 * ===================================================
 * this.box 包裹頁的容器對象
 * this.index 當前索引
 * this.length 有多少頁 最後一頁的索引爲 this.length-1
 * this.play 調用自動播放的方法 
 * this.stop 清除自動播放的方法
 * this.up 手動調用向上滑動翻頁的方法 方便增長點擊按鈕時調用
 * this.down 手動調用向下滑動翻頁的方法
 * ===================================================
 **/
;(function() {
    window.slidePage = function(opts) {
        opts = opts || {};
        if (opts.el === undefined) return;
        this.box = typeof opts.el === 'string' ? document.querySelector(opts.el) : opts.el;
        this.pages = this.box.children;
        this.length = this.pages.length;
        if (this.length < 1) return;
        if (opts.index > this.length - 1) opts.index = 0;

        this.body = document.getElementsByTagName('body')[0];
        this.nav = null;
        this.navs = [];
        this.navShow = !!opts.navShow || false;
        this.navClass = opts.navClass || 'slide-nav';

        this.index = this.oIndex = opts.index || 0;
        this.current = opts.current || 'current';
        this.locked = !!opts.locked || false;
        this.auto = !!opts.auto || false;
        this.auto && (this.delay = opts.delay || 5000);
        this.effect = opts.effect || 'translate';
        this.duration = opts.duration || 300;
        this.minScale = opts.minScale || 0.5;

        this.start = opts.start || function() {};
        this.move = opts.move || function() {};
        this.end = opts.end || function() {};

        this.timer = null;
        this.animated = true;
        this.touch = {};
        this.point = '';

        this.init();
    };
    slidePage.prototype = {
        init: function() {
            this.navShow && this.createNav();
            this.initSet();
            this.bind();
        },
        createNav: function() {
            var li = null,
                i = 0;
            this.nav = document.createElement('ul');
            for (; i < this.length; i++) {
                li = document.createElement('li');
                this.navs.push(li);
                this.nav.appendChild(li);
            }
            this.nav.className = this.navClass;
            this.body.appendChild(this.nav);
        },
        initSet: function() {
            this.height = document.documentElement.clientHeight || document.body.clientHeight;
            if (this.css(this.box, 'position') !== 'absolute' || this.css(this.box, 'position') !== 'relative') this.box.style.position = 'relative';
            if (this.css(this.box, 'overflow') !== 'hidden') this.box.style.overflow = 'hidden';
            this.box.style.height = this.height + 'px';
            for (var i = 0; i < this.length; i++) {
                if (this.css(this.pages[i], 'display') !== 'none') this.pages[i].style.display = 'none';
                if (this.css(this.pages[i], 'position') !== 'absolute') this.pages[i].style.position = 'absolute';
                this.pages[i].style.height = this.height + 'px';
            }
            if (this.navShow) {
                this.nav.style.marginTop = -this.nav.offsetHeight / 2 + 'px';
                this.navs[this.index].className = this.current;
            }
            this.pages[this.index].className += ' ' + this.current;
            this.zIndex = parseInt(this.css(this.pages[this.index], 'zIndex') === 'auto' ? 1 : this.css(this.pages[this.index], zIndex)) + 10;
            this.pages[this.index].style.display = 'block';
        },
        bind: function() {
            var _this = this;
            this.on(this.box, ['touchstart', 'pointerdown', 'mousedown'], function(e) {
                _this.touchStart(e);
                _this.auto && _this.stop();
            });
            this.on(this.box, ['touchmove', 'pointermove', 'mousemove'], function(e) {
                _this.touchMove(e);
                _this.auto && _this.stop();
            });
            this.on(this.box, ['touchend', 'touchcancel', 'pointerup', 'mouseup'], function(e) {
                _this.touchEnd(e);
                _this.auto && _this.play();
            });
        },
        touchStart: function(e) {
            this.point = e.type.indexOf('down') < 0 ? 'touch' : 'pointer';
            if (this.point === 'pointer') {
                this.touch.x = e.pageX;
                this.touch.y = e.pageY;
            }else if (this.point === 'touch') {
                this.touch.x = e.touches[0].pageX;
                this.touch.y = e.touches[0].pageY;
            }
            this.touch.disX = 0;
            this.touch.disY = 0;
            this.touch.fixed = '';
            this.start && this.start();
        },
        touchMove: function(e) {
            e.stopPropagation();
            e.preventDefault();
            if (this.point === '') return;
            if (this.touch.fixed === 'left') return;
            if (!this.animated) return;
            if (this.point === 'pointer') {
                this.touch.disX = e.pageX - this.touch.x;
                this.touch.disY = e.pageY - this.touch.y;
            }else if (this.point === 'touch') {
                if (e.touches.length > 1) return;
                this.touch.disX = e.touches[0].pageX - this.touch.x;
                this.touch.disY = e.touches[0].pageY - this.touch.y;
            }
            if (this.touch.fixed === '') {
                if (Math.abs(this.touch.disY) > Math.abs(this.touch.disX)) {
                    this.touch.fixed = 'up';
                } else {
                    this.touch.fixed = 'left';
                }
            }
            if (this.touch.fixed === 'up') {
                if (this.effect === 'scale') {
                    this.scale = ((this.height - Math.abs(this.touch.disY)) / this.height).toFixed(3);
                    this.scale < this.minScale && (this.scale = this.minScale);
                }
                if (this.touch.disY > 0) {
                    if (this.locked && this.oIndex === 0) return;
                    this.dis = -this.height;
                    this.index = this.oIndex - 1;
                    this.index < 0 && (this.index = this.length - 1);
                    if (this.effect === 'scale') this.setOrigin(this.oIndex, 'center bottom');
                } else {
                    if (this.locked && this.oIndex === this.length - 1) return;
                    this.dis = this.height;
                    this.index = this.oIndex + 1;
                    this.index > this.length - 1 && (this.index = 0);
                    if (this.effect === 'scale') this.setOrigin(this.oIndex, 'center top');
                }

                if (this.nIndex !== undefined && this.nIndex !== this.index && this.nIndex !== this.oIndex) {
                    this.pages[this.nIndex].style.display = 'none';
                    this.pages[this.nIndex].style.zIndex = '';
                    this.pages[this.nIndex].style.webkitTransform = this.pages[this.nIndex].style.transform = '';
                }
                this.nIndex = this.index;

                this.pages[this.oIndex].style.zIndex = this.zIndex;
                this.pages[this.index].style.zIndex = this.zIndex + 10;
                this.setTransform(this.index, this.dis);
                this.pages[this.index].style.display = 'block';

                this.setTransform(this.index, this.touch.disY + this.dis);
                if (this.effect === 'translate') this.setTransform(this.oIndex, this.touch.disY);
                if (this.effect === 'scale') this.setScale(this.oIndex, this.scale);

                this.move && this.move();
            }
        },
        touchEnd: function(e) {
            this.point = '';
            if (this.index === this.oIndex) return;
            if (this.touch.fixed === 'up') {
                var Y = Math.abs(this.touch.disY);
                if ((this.animated && Y > 10) || Y > this.height / 2) {
                    this.slide();
                } else {
                    this.goback();
                }
                this.end && this.end();
            }
        },
        css: function(o, n) {
            return getComputedStyle(o, null)[n];
        },
        on: function(el, types, handler) {
            for (var i = 0, l = types.length; i < l; i++) el.addEventListener(types[i], handler, false);
        },
        setScale: function(index, v) {
            this.setStyle(this.pages[index], 'transform', 'scale(' + v + ')');
        },
        setOrigin: function(index, dir) {
            this.setStyle(this.pages[index], 'transform-origin', dir);
        },
        setTransform: function(index, v) {
            this.setStyle(this.pages[index], 'transform', 'translate3d(0,' + v + 'px,0)');
        },
        setTransition: function(index, v) {
            this.setStyle(this.pages[index], 'transition', 'all ' + v + 'ms');
        },
        setStyle: function(el, p, v) {
            var prefix = ['o', 'moz', 'ms', 'webkit', ''],
                i = 0,
                l = prefix.length;
            for (; i < l; i++) {
                (function(i) {
                    var s = prefix[i] + '-' + p;
                    s = s.replace(/-\D/g, function(match) {
                        return match.charAt(1).toUpperCase();
                    });
                    el.style[s] = v;
                }(i));
            }
        },
        slide: function() {
            var _this = this;
            this.animated = false;
            this.setTransition(this.index, this.duration);
            this.setTransition(this.oIndex, this.duration);
            this.setTransform(this.index, 0);
            if (this.effect === 'translate') this.setTransform(this.oIndex, -this.dis);
            if (this.effect === 'scale') this.setScale(this.oIndex, this.minScale);
            setTimeout(function() {
                if (_this.index !== _this.oIndex) _this.update();
                _this.animated = true;
            }, this.duration);
        },
        goback: function() {
            var _this = this;
            this.setTransition(this.index, 100);
            this.setTransition(this.oIndex, 100);
            this.setTransform(this.index, this.dis);
            if (this.effect === 'translate') this.setTransform(this.oIndex, 0);
            if (this.effect === 'scale') this.setScale(this.oIndex, 1);
            setTimeout(function() {
                _this.clear();
                _this.pages[_this.index].style.display = 'none';
                _this.index = _this.oIndex;
            }, 100);
        },
        update: function() {
            if (this.navShow) {
                this.navs[this.index].className = this.current;
                this.navs[this.oIndex].className = '';
            }
            this.pages[this.oIndex].style.display = 'none';
            this.pages[this.index].className += ' ' + this.current;
            this.pages[this.oIndex].className = this.pages[this.oIndex].className.replace(this.current, '').trim();
            this.clear();
            this.oIndex = this.index;
        },
        clear: function() {
            this.pages[this.index].style.webkitTransition = this.pages[this.index].transition = '';
            this.pages[this.oIndex].style.webkitTransition = this.pages[this.oIndex].transition = '';
            this.pages[this.index].style.webkitTransform = this.pages[this.index].style.transform = '';
            this.pages[this.oIndex].style.webkitTransform = this.pages[this.oIndex].style.transform = '';
            this.pages[this.oIndex].style.zIndex = '';
            this.pages[this.index].style.zIndex = '';
            if (this.effect === 'scale') this.setOrigin(this.oIndex, '');
        },
        animate: function() {
            var _this = this;
            this.setTransform(this.index, this.dis);
            this.pages[this.index].style.display = 'block';
            this.pages[this.oIndex].style.zIndex = this.zIndex;
            this.pages[this.index].style.zIndex = this.zIndex + 10;
            this.setTransition(this.index, 0);
            this.setTransition(this.oIndex, 0);
            setTimeout(function() {
                _this.slide();
            }, 50);
        },
        up: function() {
            this.dis = this.height;
            this.index++;
            this.index > this.length - 1 && (this.index = 0);
            if (this.effect === 'scale') this.setOrigin(this.oIndex, 'center top');
            this.animate();
        },
        down: function() {
            this.dis = -this.height;
            this.index--;
            this.index < 0 && (this.index = this.length - 1);
            if (this.effect === 'scale') this.setOrigin(this.oIndex, 'center bottom');
            this.animate();
        },
        play: function() {
            var _this = this;
            if (this.locked) return;
            this.timer = setInterval(function() {
                _this.up();
            }, this.delay);
        },
        stop: function() {
            this.timer && clearInterval(this.timer);
            this.timer = null;
        }
    };
}());
查看完整代碼

  和之前寫的 圖片切換 有許多共同的地方,整個翻頁和圖片切換原理都是相似的。瀏覽器

  這個支持自動播放,三種翻頁效果,限制頭尾滑動等。微信

  翻頁動畫,就是在一個容器內滑動,在這個容器中每次只顯示一頁,每一頁都有一些css3動畫效果的元素出現。css3動畫

  一個簡單的HTML結構以下:app

<div id="slidePageBox" class="slide-box">
    <section class="slide-page page1">
         <!-- 1 -->
    </section>
    <section class="slide-page page2">
        <!-- 2 -->
    </section>
    <section class="slide-page page3">
        <!-- 3 -->
    </section>
    <section class="slide-page page4">
        <!-- 4 -->
    </section>
    <section class="slide-page page5">
        <!-- 5 -->
    </section>
</div>

  類名爲slide-box的div標籤就是容器,類名爲slide-page的section標籤就是每一個要翻的頁。

this.box = typeof opts.el === 'string' ? document.querySelector(opts.el) : opts.el;
this.pages = this.box.children;
this.length = this.pages.length;

  這裏this.box就是容器,this.pages就是全部要翻的頁。

this.height = document.documentElement.clientHeight;
this.box.style.height = this.height + 'px';
for (var i = 0; i < this.length; i++) {
    //..
    this.pages[i].style.height = this.height + 'px';
}

  獲取瀏覽器窗口的高,並設置容器和頁的高爲這個值。這個翻頁是上下滑動,JS就沒有獲取寬度值了。

  容器寬用css設置爲width:100%,作個最大寬度限制爲max-width:640px,這可根據實際狀況設置。

 

    如今切換翻頁,當手指在容器滑動時,這裏爲判斷上下滑動,知足滑動必定距離,手指離開容器時,開始翻頁。

  觸摸事件:手指移入,手指移動,手指離開

  指針事件:指針按下,指針移動,指針收起

  鼠標事件: 鼠標按下,鼠標移動,鼠標收起

//..
bind: function() {
    var _this = this;
    this.on(this.box, ['touchstart', 'pointerdown', 'mousedown'], function(e) {
        _this.touchStart(e); 
        //..
    });
    this.on(this.box, ['touchmove', 'pointermove', 'mousemove'], function(e) {
        _this.touchMove(e);
        //..
    });
    this.on(this.box, ['touchend', 'touchcancel', 'pointerup', 'mouseup'], function(e) {
        _this.touchEnd(e);
        //..
    });
},
//..

  綁定了觸摸事件,WP的指針事件,鼠標事件。

//..
touchStart: function(e) {
    this.point = e.type.indexOf('down') < 0 ? 'touch' : 'pointer';
    if (this.point === 'pointer') {
        this.touch.x = e.pageX;
        this.touch.y = e.pageY;
    }else if (this.point === 'touch') {
        this.touch.x = e.touches[0].pageX;
        this.touch.y = e.touches[0].pageY;
    }
    //..
},
//..

  獲取移入時的初始位置(觸摸,指針,鼠標)

//..
touchMove: function(e) {
    //..
    if (this.touch.fixed === 'left') return;
    //..
    if (this.point === 'pointer') {
        this.touch.disX = e.pageX - this.touch.x;
        this.touch.disY = e.pageY - this.touch.y;
    }else if (this.point === 'touch') {
        //..
        this.touch.disX = e.touches[0].pageX - this.touch.x;
        this.touch.disY = e.touches[0].pageY - this.touch.y;
    }
    if (this.touch.fixed === '') {
        if (Math.abs(this.touch.disY) > Math.abs(this.touch.disX)) {
            this.touch.fixed = 'up';
        } else {
            this.touch.fixed = 'left';
        }
    }
    if (this.touch.fixed === 'up') {
        //..
    }
},
//..

  移動時(觸摸,指針,鼠標)獲取移動了多少距離,根據距離判斷向左右仍是上下移動的,這裏翻頁只要上下移動。

//..
if (this.touch.fixed === 'up') {
    if (this.effect === 'scale') {
        this.scale = ((this.height - Math.abs(this.touch.disY)) / this.height).toFixed(3);
        this.scale < this.minScale && (this.scale = this.minScale);
    }
    if (this.touch.disY > 0) {
        if (this.locked && this.oIndex === 0) return;
        this.dis = -this.height;
        this.index = this.oIndex - 1;
        this.index < 0 && (this.index = this.length - 1);
        if (this.effect === 'scale') this.setOrigin(this.oIndex, 'center bottom');
    } else {
        if (this.locked && this.oIndex === this.length - 1) return;
        this.dis = this.height;
        this.index = this.oIndex + 1;
        this.index > this.length - 1 && (this.index = 0);
        if (this.effect === 'scale') this.setOrigin(this.oIndex, 'center top');
    }

    if (this.nIndex !== undefined && this.nIndex !== this.index && this.nIndex !== this.oIndex) {
        this.pages[this.nIndex].style.display = 'none';
        this.pages[this.nIndex].style.zIndex = '';
        this.pages[this.nIndex].style.webkitTransform = this.pages[this.nIndex].style.transform = '';
    }
    this.nIndex = this.index;

    this.pages[this.oIndex].style.zIndex = this.zIndex;
    this.pages[this.index].style.zIndex = this.zIndex + 10;
    this.setTransform(this.index, this.dis);
    this.pages[this.index].style.display = 'block';

    this.setTransform(this.index, this.touch.disY + this.dis);
    if (this.effect === 'translate') this.setTransform(this.oIndex, this.touch.disY);
    if (this.effect === 'scale') this.setScale(this.oIndex, this.scale);

    this.move && this.move();
}
//..
移動中

  移動中作了不少事情,是整個程序比較重要的部分。在上下移動時,當前顯示的頁跟着移動,向上移動時,下一頁顯示出來一部分,向下移動時,上一頁顯示一部分。

  若是鎖定了頭尾,第一頁向下移動時不能移動,一樣最後一頁向上移動時不能移動。

opts.effect 動畫效果(平移=translate 縮放=scale 重疊=overlap) 默認平移

  根據每種翻頁效果作初始化設置(若是隻須要簡單的平移切換能夠省去不少代碼)。

//..
touchEnd: function(e) {
    this.point = '';
    if (this.index === this.oIndex) return;
    if (this.touch.fixed === 'up') {
        var Y = Math.abs(this.touch.disY);
        if ((this.animated && Y > 10) || Y > this.height / 2) {
            this.slide();
        } else {
            this.goback();
        }
       //..
    }
},
//..

  離開時(觸摸,指針,鼠標)根據條件執行滑動切換。

//..
slide: function() {
    var _this = this;
    this.animated = false;
    this.setTransition(this.index, this.duration);
    this.setTransition(this.oIndex, this.duration);
    this.setTransform(this.index, 0);
    if (this.effect === 'translate') this.setTransform(this.oIndex, -this.dis);
    if (this.effect === 'scale') this.setScale(this.oIndex, this.minScale);
    setTimeout(function() {
        if (_this.index !== _this.oIndex) _this.update();
        _this.animated = true;
    }, this.duration);
},
//..

  滑動切換完成時會更新一些設置,好比爲當前頁增長當前標誌類名。每頁的css3動畫元素也就能夠根據這個類名執行動畫效果。

//..
update: function() {
    if (this.navShow) {
        this.navs[this.index].className = this.current;
        this.navs[this.oIndex].className = '';
    }
    this.pages[this.oIndex].style.display = 'none';
    this.pages[this.index].className += ' ' + this.current;
    this.pages[this.oIndex].className = this.pages[this.oIndex].className.replace(this.current, '').trim();
    this.clear();
    this.oIndex = this.index;
},
//..

  切換了頁,執行了每頁的動畫,整個場景動畫就差很少完成了。

    最後就是一些簡單的了,自動播放什麼的。

  好了,到此結束。

   ---------------- 補充 ------------------------

   爲了兼容各大瀏覽器,css3動畫屬性前綴值得注意。

//..
setStyle: function(el, p, v) {
    !this.cache[el] && (this.cache[el] = {});
    !this.cache[el][p] && (this.cache[el][p] = this.prefix(p));
    el.style[this.cache[el][p] || this.prefix(p)] = v;
},
prefix: function(p) {
    var style = document.createElement('div').style;
    if (p in style) return p;
    var prefix = ['webkit', 'Moz', 'ms', 'O'],
        i = 0,
        l = prefix.length,
        s = '';
    for (; i < l; i++) {
        s = prefix[i] + '-' + p;
        s = s.replace(/-\D/g, function(match) {
            return match.charAt(1).toUpperCase();
        });
        if (s in style) return s;
    }
},
//..

  這裏作了PC端查看兼容,注意IE的pointer移動事件會有個頗有意思的表現,移動端的程序仍是在移動設備中查看比較好。