最近有個任務,做一個非常小的h5的應用,只有2屏,需要做橫向的全屏滑動切換和一些簡單的動畫效果,之前做這種東西用的是fullpage.js和jquery,性能不是很好,於是就想自己動手弄一個簡單的東西來實現。最後我用zepto + hammer.js 和輪播的方式解決了這個問題,效果還不錯,整個頁面不開啟Gzip時所有資源請求的數據大小為200KB左右。這篇文章總結下這個方法的實現思路。
效果演示:
1. 實現要點
1)滑屏借鑒bootstrap的carousel插件,不過完全沒有它那個復雜,只需要借鑒它的輪播實現思路即可;
2)滑屏切換的觸發,跟PC不一樣,PC通常都是通過元素的點擊回調來觸發,對於滑屏的頁面,完全可以利用window的hashchange事件來處理,這樣只要通過超鏈接設置錨點或者通過js改變location.hash就能觸發切換;
3)考慮到移動還得支持手勢操作,可以使用hammer.js這個手勢庫,API非常簡單易用;
4)動畫效果可以用animate.css,不過不用把它所有的代碼都弄到代碼裡,只需要拷貝需要的動畫效果相關的代碼即可;
5)替代jquery,首選zepto;
6)滑屏效果使用transition動畫,為了能夠響應動畫結束的回調,可以考慮使用transition.js,這個也是Bootstrap提供的工具,不過它默認只能跟jquery使用,要對它稍微改變一下才能跟zepto聯合使用。
這些要點說的比較粗糙,後面的內容會一一詳細介紹。
2. html結構
空的滑屏頁的html結構是這樣的:
<div id="container" class="container"> <section id="page-1" class="page page--1"> </section> <section id="page-2" class="page page--2"> </section> <section id="page-3" class="page page--3"> </section> </div>
html,
body { height: 100%; -webkit-tap-highlight-color: transparent; } .container, .page { position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; } .page { overflow: hidden; display: none; -webkit-transition: -webkit-transform .4s ease; transition: transform .4s ease; -webkit-backface-visibility: hidden; backface-visibility: hidden; }
.container與.page初始化的時候采用絕對定位,全屏布局。每一個section.page代表一頁,並且默認不顯示,所有頁的定位都相同,也就是說如果所有頁都顯示的話,這些頁會重疊在一塊。
demo頁的html結構是:
<div id="container" class="container"> <section id="page-1" class="page page--1"> <div class="page__jump"><a href="#page-2" title="">下一頁</a></div> <p class="page__num animated">1</p> </section> <section id="page-2" class="page page--2"> <div class="page__jump"><a href="#page-1" title="">上一頁</a><a href="#page-3" title="">下一頁</a></div> <p class="page__num animated">2</p> </section> <section id="page-3" class="page page--3"> <div class="page__jump"><a href="#page-2" title="">上一頁</a></div> <p class="page__num animated">3</p> </section> </div>
demo相關的css就不展示了。其中animated是應用animate.css需要的,animate.css是一個動畫庫,github上有。
3. 滑屏切換的實現思路
滑屏切換就是通過js控制2個要滑動的頁增加和刪除以下定義的這一些css類實現的:
.page.page--active, .page.page--prev, .page.page--next { display: block; } .page.page--next, .page.page--active.page--active-right { -webkit-transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0); } .page.page--prev, .page.page--active.page--active-left { -webkit-transform: translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0); } .page.page--next.page--next-left, .page.page--prev.page--prev-right, .page.page--active { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); }
.page--active表示當前顯示的頁,頁面初始化後,通過以下js調用,給第一頁加上.page—active:
var $activePage; //初始化顯示第一頁 (function () { $activePage = $('#page-1'); $activePage.addClass('page--active'); })();
這樣頁面默認就顯示了第一頁。以向左滑屏說明這些css的使用原理:
第一步,找到下一頁的section,添加page--next類,將它定位當前頁的右邊,為滑屏做准備;
第二步,找到當前頁的section,給它添加page--active-left,由於這個類改變了translate3D屬性的值,所以當前頁會往左滑動一屏;
在第二步的同時,給下一頁的section,添加page--next-left,由於這個類改變了translate3D屬性的值,所以下一頁會往左滑動一屏;
第三步,在當前頁跟下一頁滑屏動畫結束後,找到原來的當前頁,移除掉page--active和page--active-left類;
在第三步的同時,找到下一頁,移除掉page--next和page--next-left類,添加page--active。
gif圖說明如下:
向右滑屏原理類似:
第一步,找到上一頁的section,添加page--prev類,將它定位當前頁的左邊,為滑屏做准備;
第二步,找到當前頁的section,給它添加page--active-right,由於這個類改變了translate3D屬性的值,所以當前頁會往右滑動一屏;
在第二步的同時,給上一頁的section,添加page--prev-right,由於這個類改變了translate3D屬性的值,所以上一頁會往右滑動一屏;
第三步,在當前頁跟上一頁滑屏動畫結束後,找到原來的當前頁,移除掉page--active和page--active-right類;
在第三步的同時,找到上一頁,移除掉page--prev和page--prev-right類,添加page--active。
綜合以上實現原理,封裝成JS函數如下:
var TRANSITION_DURATION = 400, sliding = false; function getSlideType($targetPage) { var activePageId = $activePage.attr('id'), targetPageId = $targetPage.attr('id'); return activePageId < targetPageId ? 'next' : activePageId == targetPageId ? '' : 'prev'; } function slide(targetPageId) { var $targetPage = $('#' + targetPageId); if (!$targetPage.length || sliding) return; var slideType = getSlideType($targetPage), direction = slideType == 'next' ? 'left' : 'right'; if (slideType == '') return; sliding = true; $targetPage.addClass('page--' + slideType); $targetPage[0].offsetWidth; $activePage.addClass('page--active-' + direction); $targetPage.addClass('page--' + slideType + '-' + direction); $activePage .one($.transitionEnd.end, function () { $targetPage.removeClass(['page--' + slideType, 'page--' + slideType + '-' + direction].join(' ')).addClass('page--active'); $activePage.removeClass(['page--active', 'page--active-' + direction].join(' ')); $activePage = $targetPage; sliding = false; }) .emulateTransitionEnd(TRANSITION_DURATION); }
由於$activePage在頁面初始化的時候默認指定為第一頁,在每次滑屏結束後都會更新成最新的當前頁,所以調用的時候只要把目標頁的ID傳給slide函數即可。以上代碼可能會有疑問的是:
1)$targetPage[0].offsetWidth的作用,這個代碼用來觸發浏覽器的重繪,因為目標頁原來是display: none的,如果不觸發重繪的話,下一步添加css類後將看不到動畫效果;
2)$.transitionEnd.end以及emulateTransitionEnd的作用,這個在下一部分說明。
4. 浏覽器css動畫結束的回調及模擬
bootstrap提供了一個工具,transition.js,用來判斷浏覽器是否支持css動畫回調事件,以及在浏覽器沒有在動畫結束後自動觸發回調的特殊情況下通過模擬的方式來手動觸發回調,原先這個工具只能配合jquery使用,為了在zepto中使用,必須稍微改變一下,下面就是改變之後的代碼:
(function(){ var transition = $.transitionEnd = { end: (function () { var el = document.createElement('transitionEnd'), transEndEventNames = { WebkitTransition: 'webkitTransitionEnd', MozTransition: 'transitionend', OTransition: 'oTransitionEnd otransitionend', transition: 'transitionend' }; for (var name in transEndEventNames) { if (el.style[name] !== undefined) { return transEndEventNames[name]; } } return false; })() }; $.fn.emulateTransitionEnd = function (duration) { var called = false, _this = this, callback = function () { if (!called) $(_this).trigger(transition.end); }; $(this).one(transition.end, function () { called = true; }); setTimeout(callback, duration); return this; }; })();
$.transitionEnd.end表示當前浏覽器支持的動畫結束事件的名稱。$.fn.emulateTransitionEnd是一個擴展了Zepto原型的一個方法,傳入一個動畫的過渡時間,當這個時間段過完之後,如果浏覽器沒有自動觸發回調事件,called就始終是false,setTimeout會導致callback被調用,然後callback內部就會手動觸發動畫結束的回調。為什麼要通過這個方式來模擬動畫結束,是因為浏覽器即使支持動畫結束事件的回調,但是有些時候並不會觸發這個事件,或者在動畫結束後不能立即觸發,影響回調的准確性。傳入的duration應該與執行動畫的元素,在css上設置的transtion-duration相同,注意以下代碼中標黃的部分:
var TRANSITION_DURATION = 400 ; $activePage .one($.transitionEnd.end, function () { $targetPage.removeClass(['page--' + slideType, 'page--' + slideType + '-' + direction].join(' ')).addClass('page--active'); $activePage.removeClass(['page--active', 'page--active-' + direction].join(' ')); $activePage = $targetPage; sliding = false; }) .emulateTransitionEnd(TRANSITION_DURATION); .page { overflow: hidden; display: none; -webkit-transition: -webkit-transform .4s ease; transition: transform .4s ease; -webkit-backface-visibility: hidden; backface-visibility: hidden; }
5. hashchange事件
PC端滑屏都是給元素添加點擊事件觸發的,移動端可以利用window的hashchange事件:
$(window).on('hashchange', function (e) { var hash = location.hash; if (!hash) hash = '#page-1'; slide(hash.substring(1)); }); location.hash = '#page-1';
hashchange事件,在js代碼中通過改變loaction.hash或者是點擊<a href="#page-2" title="">下一頁</a>這樣的超鏈接時,都會觸發,所以只要在這個事件的回調去做滑屏切換即可。這樣那些上一頁和下一頁的鏈接元素都不用加事件了。
6. hammer.js使用簡介
hammer.js是一個手勢庫,支持常用的手勢操作,使用簡單,引入它的js之後,通過以下的方式來支持手勢滑屏:
//初始化手勢滑動 var container = document.getElementById('container'), mc = new Hammer.Manager(container), Swipe = new Hammer.Swipe(); mc.add(Swipe); mc.on('swipeleft', function (e) { swipteTo('next', e); }); mc.on('swiperight', function (e) { swipteTo('prev', e); }); function swipteTo(slideType, e) { var $targetPage = $activePage[slideType]('.page'); $targetPage.length && (location.hash = '#' + $targetPage.attr('id')); }
把整個container元素作為滑屏的stage,監聽到swipeleft事件,就表示向左滑,頁面應該顯示下一頁;監聽到swiperight事件,就表示向右滑,頁面應該顯示下一頁。
7. 結束語
animate.css的使用就不詳細介紹了,比較簡單,這是它的github地址:https://github.com/daneden/animate.css,是一個非常好用的動畫庫。本文把最近的一點工作經驗記錄了下來,技術上的東西,有的時候一些文字不能完全講的清楚,所以我只能盡自己的能力去把一些問題講地稍微細致一點,說的不對和有問題的盡管在評論區與我說明,我會認真查看,另外我自己對移動端這一塊入門不深,您有更好的見解,歡迎與我們一起分享。謝謝您的閱讀,馬上就新年,祝您猴年大吉!