前言
原文:原生實(shí)現(xiàn)一個(gè)手勢視差翻頁器 #147
一句話需求
移動(dòng)端實(shí)現(xiàn)一個(gè)輪播器,在輪播器上層有一個(gè)靜止圖層汤求,不會(huì)隨著輪播器翻頁而偏移卖丸。
以下是兩個(gè)已經(jīng)實(shí)踐的兩個(gè)項(xiàng)目:
[Github]洋满,歡迎各方Gay Man來Fork與StarP⒑铡!几迄!
?
需求分析
以上需求可以拆分成兩個(gè)小的需求:
- 實(shí)現(xiàn)一個(gè)輪播器表蝙;
- 做個(gè)靜止圖層。
問題
輪播器作為一個(gè)圖層乓旗,靜止圖層是一個(gè)。
那么那個(gè)圖層在上集索,那個(gè)在下屿愚?
- 輪播器在上汇跨,則靜止圖層被輪播器遮蓋;
- 靜止圖層在上妆距,則綁定在輪播器上的手勢事件由于被靜止圖層在上而無法觸發(fā)穷遂。
再細(xì)分需求
為了顯示靜止圖層,那么靜止圖層一定是要在上的娱据。那么就必須解決第2個(gè)問題蚪黑,怎么觸發(fā)輪播器的手勢。
我們可以通過監(jiān)聽靜止圖層或者兩圖層容器的手勢中剩,然后映射到輪播器忌穿。
?
實(shí)現(xiàn)
1 實(shí)現(xiàn)一個(gè)輪播器
由于網(wǎng)上的輪播器插件的都是封裝好的,觸發(fā)事件比較麻煩结啼,因此選擇自己使用原生js實(shí)現(xiàn)一個(gè)掠剑。
實(shí)現(xiàn)輪播器的方案有很多中,比如改變translate
屬性的郊愧,也有先實(shí)現(xiàn)一個(gè)scroll-view
朴译,然后改變scroll-view
的scrollLeft
。我選用后者(單純是因?yàn)槲覜]有這樣做過属铁,而且好像很有趣)眠寿。
那么就先實(shí)現(xiàn)一個(gè)scroll-view
。
一般scroll-view
都是縱向的焦蘑,根據(jù)以上需求盯拱,我們就需要實(shí)現(xiàn)一個(gè)橫向的scroll-view
,具體實(shí)現(xiàn)如下
<div id="app">
<dl class="story">
<dt></dt><dt></dt>
<dt></dt><dt></dt><dt></dt>
<dt></dt><dt></dt>
</dl>
</div>
*{ margin: 0;padding: 0; }
html,body{ height: 100%; }
::-webkit-scrollbar{ display: none; }
#app{ height: 100%;position: relative; }
#app > .story{ position: absolute;top: 0;right: 0;bottom: 0;left: 0; }
#app > .story{
z-index: 1000; border: 1px solid;
overflow-x: auto;overflow-y: hidden; white-space: nowrap;font-size: 0;letter-spacing: 0;
/* translateZ是是為了開啟硬件加速 */
-webkit-transform: translateZ(0);
-moz-transform: translateZ(0);
-ms-transform: translateZ(0);
-o-transform: translateZ(0);
transform: translateZ(0);
}
.story > dt{ height: 100%;width: 100%;display: inline-block;vertical-align: top;position: relative; }
.story > dt:nth-of-type(1){ background-color: red; }
.story > dt:nth-of-type(2){ background-color: orange; }
.story > dt:nth-of-type(3){ background-color: yellow; }
.story > dt:nth-of-type(4){ background-color: green; }
.story > dt:nth-of-type(5){ background-color: cyan; }
.story > dt:nth-of-type(6){ background-color: blue; }
.story > dt:nth-of-type(7){ background-color: purple; }
核心css為
overflow-x: auto;overflow-y: hidden; white-space: nowrap;
禁用塊級元素的換行喇肋,依次達(dá)到橫排的效果坟乾。
[查看源碼:jsbin]
[查看源碼:jsfiddle]
(ps:要看效果的gay man請點(diǎn)擊[查看源碼:jsbin],并使用移動(dòng)設(shè)備模式查看蝶防, 下同)
?
2 實(shí)現(xiàn)靜止圖層并監(jiān)聽其上的手勢
先向#app中添加靜止圖層
<div id="app">
<dl class="mash">
<img src="http://ohi69gup6.bkt.clouddn.com/005TGG6vly1fes9jc0kk0g30b40b40tv.gif">
</dl>
...
</div>
然后甚侣,來分析一下輪播器的功能
- 向左,向右滑動(dòng)翻頁间学;
- 輪播器卡片手勢跟隨殷费。
第1點(diǎn)的實(shí)現(xiàn)思路
獲取手指 “觸碰屏幕那刻” 和 “離開屏幕那刻” 的位置,根據(jù)以上兩個(gè)位置判斷翻頁方向低葫,要獲取這兩位置详羡,就需要分別注冊touchstart
和touchend
事件,通過事件的回調(diào)參數(shù)( event
)獲取嘿悬。
// 觸碰屏幕那刻橫向位置
var touchStartX = e.changedTouches[0].pageX;
// 離開屏幕那刻橫向位置
var touchEndX = e.changedTouches[0].pageX;
判斷滑動(dòng)方向
var isNext = touchEndX - touchStartX < 0;
知道翻頁方向就可以進(jìn)行翻頁
翻頁如上文所說实柠,通過改變卡片容器的scrollLeft
。這里的容器是#app > .story
善涨。
document.querySelector("#app > .story").scrollLeft
想要實(shí)現(xiàn)翻頁窒盐,還必須知道兩個(gè)屬性:
- 每頁寬度:可以通過
story.clientWidth
獲炔菰颉; - 當(dāng)前頁:使用全局變量記錄當(dāng)前是第幾頁蟹漓。
具體實(shí)現(xiàn)如下:
var touchStartX = 0;
var currentPage = 1;
var mash = document.querySelector("#app > .mash");
var story = document.querySelector("#app > .story");
var screenWidth = story.clientWidth;
mash && mash.addEventListener('touchstart', function(e){
var touch = e.changedTouches[0];
touchStartX = touch.pageX;
}, false);
mash && mash.addEventListener('touchend', function(e){
var touch = e.changedTouches[0];
var touchEndX = touch.pageX;
var isNext = touchEndX - touchStartX < 0;
var oldCurrentPage = currentPage;
var maxPage = story.children.length;
var minPage = 1;
currentPage = isNext ? currentPage + 1 : currentPage - 1;
currentPage = currentPage > maxPage ? maxPage : currentPage;
currentPage = currentPage < minPage ? minPage : currentPage;
var targetScrollLeft = (currentPage - 1) * screenWidth;
story.scrollLeft = targetScrollLeft;
}, false);
以上代碼基本實(shí)現(xiàn)翻頁炕横,但是僅僅是通過每次 “增加/減少 一個(gè)screenWidth
的距離” 達(dá)到翻頁的目的,是不夠的葡粒,因?yàn)槟銜?huì)發(fā)現(xiàn)翻頁效果很生硬的份殿,僅僅只有一幀。
下面讓我們把翻頁效果做得更加順滑嗽交。
怎么做卿嘲,使用定時(shí)器(setTimeout
)?
不轮纫!不腔寡!不!當(dāng)然不會(huì)掌唾!
我們使用requestAnimationFrame
放前,至于為什么使用requestAnimationFrame
而不使用setTimeout
?請自行Google糯彬。接著凭语,讓我們先實(shí)現(xiàn)一個(gè)動(dòng)畫函數(shù)
function animate(callback) {
var _animate = function(){
var isValid = callback && callback();
isValid && requestAnimationFrame(_animate);
};
requestAnimationFrame(_animate);
}
ok,然后要做的就是將它運(yùn)用到翻頁的代碼中
// 設(shè)置每幀改變scrollLeft距離
var step = 30;
// ...
mash && mash.addEventListener('touchend', function(e){
// ...
animate(function() {
var scrollLeft = story.scrollLeft;
scrollLeft = isNext ? scrollLeft + step : scrollLeft - step;
if(isNext && scrollLeft > targetScrollLeft) {
story.scrollLeft = targetScrollLeft;
return false;
} else if(!isNext && scrollLeft < targetScrollLeft){
story.scrollLeft = targetScrollLeft;
return false;
} else {
story.scrollLeft = scrollLeft;
return true;
}
});
}, false);
第2點(diǎn)的實(shí)現(xiàn)思路
第二點(diǎn)需要實(shí)現(xiàn)的功能是:輪播器卡片手勢跟隨撩扒。
假如你有試過以上代碼實(shí)現(xiàn)的輪播器似扔,你會(huì)發(fā)現(xiàn)一個(gè)問題:手指在未離開屏幕前,無論手指怎么滑動(dòng)都不會(huì)有任何響應(yīng)搓谆,這明顯不是我們想要的炒辉。標(biāo)準(zhǔn)的輪播器應(yīng)該是:當(dāng)前卡片應(yīng)該對手勢做出響應(yīng),卡片跟隨手指移動(dòng)泉手。
要實(shí)現(xiàn)以上效果黔寇,就需要監(jiān)聽手指未離開屏幕的前的動(dòng)作,注冊touchmove
事件斩萌,獲取手指每次移動(dòng)的位置缝裤!
mash && mash.addEventListener('touchmove', function(e){
var touch = e.changedTouches[0];
var moveingfPageX = touch.pageX;
var distance = -(moveingfPageX - touchStartX);
var currentPageScrollLeft = (currentPage - 1) * screenWidth;
story.scrollLeft = currentPageScrollLeft + distance;
}, false);
思路也是比較簡單的,監(jiān)聽手指所在位置與初始位置touchStartX
的距離颊郎,然后動(dòng)態(tài)地改變story.scrollLeft
憋飞。
另外,現(xiàn)在已經(jīng)實(shí)現(xiàn)卡片的手勢跟隨姆吭,隨之又可能有這么一種情況:
用戶已經(jīng)滑動(dòng)卡片榛做,但是在滑動(dòng)后用戶不想翻頁了,轉(zhuǎn)而想繼續(xù)留在當(dāng)前頁,即 “后悔功能”瘤睹。然后再看看當(dāng)前我們的邏輯:
// ...
var isNext = touchEndX - touchStartX < 0;
// ...
currentPage = isNext ? currentPage + 1 : currentPage - 1;
currentPage = currentPage > maxPage ? maxPage : currentPage;
currentPage = currentPage < minPage ? minPage : currentPage;
以上邏輯是:如果不是上一頁就是下一頁升敲。并沒有實(shí)現(xiàn)后悔功能。
為實(shí)現(xiàn)后悔功能轰传,我做如下修改
// 設(shè)置最小有效翻頁距離(在手指離開屏幕時(shí)所偏移的橫向距離),換言之瘪撇,偏移距離小于30就觸發(fā)后悔功能
var validFlipDistance = 30;
// ...
mash && mash.addEventListener('touchend', function(e){
// ...
var isValid = Math.abs(touchEndX - touchStartX) >= validFlipDistance;
// ...
if(isValid) {
currentPage = isNext ? currentPage + 1 : currentPage - 1;
currentPage = currentPage > maxPage ? maxPage : currentPage;
currentPage = currentPage < minPage ? minPage : currentPage;
} else {
// 觸發(fā)后悔功能获茬,改變滑動(dòng)方向,由animate函數(shù)將卡片反向滑回初始位置
isNext = !isNext;
}
});
[查看源碼: jsbin]
[查看源碼: jsfiddle]
?
寫在最后
我知道自己的實(shí)現(xiàn)并不是最佳實(shí)踐倔既,但是我想說的是:造輪子真爽恕曲!
?