最近有個任務,做一個非常小的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或者是点击2fc8db3327435aa54ad74d366b04c0cb下一页5db79b134e9f6b82c0b36e0489ee08ed这样的超链接时,都会触发,所以只要在这个事件的回调去做滑屏切换即可。这样那些上一页和下一页的链接元素都不用加事件了。
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,是一個非常好用的動畫庫。本文把最近的一點工作經驗記錄了下來,技術上的東西,有的時候一些文字不能完全講的清楚,所以我只能盡自己的能力去把一些問題講地稍微細緻一點,說的不對和有問題的儘管在評論區與我說明,我會認真查看,另外我自己對移動端這一塊入門不深,您有更好的見解,歡迎與我們一起分享。謝謝您的閱讀,馬上就新年,祝您猴年大吉!