移動端效果之Swiper詳解
寫在前面
最近在做移動端方面運用到了餓了么的vue前端組件庫,因為不想單純用組件而使用它,故想深入了解一下實現(xiàn)原理。后續(xù)將會繼續(xù)研究一下其他的組件實現(xiàn)原理,有興趣的可以關(guān)注下。

代碼在這里:戳我
1. 說明
父容器overflow:hidden;,子頁面transform:translateX(-100%);width:100%;
2. 核心解析
2.1 頁面初始化
由于所有頁面都在手機屏幕左側(cè)一個屏幕寬度的位置,因此最開始的情況是頁面中看不到任何一個子頁面,所以第一步應(yīng)該設(shè)置應(yīng)該顯示的子頁面,默認(rèn)情況下defaultIndex:0
function reInitPages() {
// 得出頁面是否能夠被滑動
// 1. 子頁面只有一個
// 2. 用戶手動設(shè)置不能滑動 noDragWhenSingle = true
noDrag = children.length === 1 && noDragWhenSingle;
var aPages = [];
var intDefaultIndex = Math.floor(defaultIndex);
var defaultIndex = (intDefaultIndex >= 0 && intDefaultIndex < children.length)
? intDefaultIndex : 0;
// 得到當(dāng)前被激活的子頁面索引
index = defaultIndex;
children.forEach(function(child, index) {
aPages.push(child);
// 所有頁面移除激活class
child.classList.remove('is-active');
if (index === defaultIndex) {
// 給激活的子頁面加上激活class
child.classList.add('is-active');
}
});
pages = aPages;
}
2.2 容器滑動開始(onTouchStart)
在低版本的android手機上,設(shè)置event.preventDefault()會起到一定的性能提升作用,使得滑動起來不是那么卡。
前置工作:
- 如果用戶設(shè)置了 prevent:true, 滑動時阻止默認(rèn)行為
- 如果用戶設(shè)置了stopPropagation:true, 滑動時阻止事件向上傳播
- 如果動畫尚未結(jié)束,阻止滑動
- 設(shè)置dragging:true,滑動開始
- 設(shè)置用戶滾動為false
滑動開始:
使用一個全局對象記錄信息,這些信息包括:
dragState = {
startTime // 開始時間
startLeft // 開始的X坐標(biāo)
startTop // 開始的Y坐標(biāo)(相對于整個頁面viewport pageY)
startTopAbsolute // 絕對Y坐標(biāo)(相對于文檔頂部 clientY)
pageWidth // 一個頁面寬度
pageHeight // 一個頁面的高度
prevPage // 上一個頁面
dragPage // 當(dāng)前頁面
nextPage // 下一個頁面
};
2.3 容器滑動(onTouchMove)
套用全局dragState,記錄新的信息
dragState = {
currentLeft // 開始的X坐標(biāo)
currentTop // 開始的Y坐標(biāo)(相對于整個頁面viewport pageY)
currentTopAbsolute // 絕對Y坐標(biāo)(相對于文檔頂部 clientY)
};
那么我們就可以通過開始和滑動中的信息來計算出一些東西:
滑動的水平位移(offsetLeft = currentLeft - startLeft)
滑動的垂直位移(offsetTop = currentTopAbsolute - startTopAbsolute)
是否是用戶的自然滾動,這里的自然滾動說的是用戶并不是想滑動swiper,而是想滑動頁面
// 條件 // distanceX = Math.abs(offsetLeft); // distanceY = Math.abs(offsetTop); distanceX < 5 || ( distanceY >= 5 && distanceY >= 1.73 * distanceX )
判斷是左移還是右移(offsetLeft < 0 左移,反之,右移)
重置位移
// 如果存在上一個頁面并且是左移
if (dragState.prevPage && towards === 'prev') {
// 重置上一個頁面的水平位移為 offsetLeft - dragState.pageWidth
// 由于 offsetLeft 一直在變化,并且 >0
// 那么也就是說 offsetLeft - dragState.pageWidth 的值一直在變大,但是仍未負(fù)數(shù)
// 這就是為什么當(dāng)連續(xù)屬性存在的時候左滑會看到上一個頁面會跟著滑動的原因
// 這里的 translate 方法其實很簡單,在滑動的時候去除了動畫效果`transition`,單純改變位移
// 而在滑動結(jié)束的時候,加上`transition`,使得滑動到最后釋放的過渡更加自然
translate(dragState.prevPage, offsetLeft - dragState.pageWidth);
}
// 當(dāng)前頁面跟著滑動
translate(dragState.dragPage, offsetLeft);
// 后一個頁面同理
if (dragState.nextPage && towards === 'next') {
translate(dragState.nextPage, offsetLeft + dragState.pageWidth);
}
2.4 滑動結(jié)束(onTouchEnd)
前置工作:
在滑動中,我們是可以實時地來判斷到底是不是用戶的自然滾動userScrolling,如果是用戶自然滾動,那么swiper的滑動信息就不算數(shù),因此要做一些清除操作:
dragging = false;
dragState = {};
當(dāng)然如果userScrolling:false,那么就是滑動子頁面,執(zhí)行doOnTouchEnd方法
判斷是否是tap事件
// 時間小于300ms,click事件延遲300ms觸發(fā)
// 水平位移和垂直位移棟小于5像素
if (dragDuration < 300) {
var fireTap = Math.abs(offsetLeft) < 5 && Math.abs(offsetTop < 5);
if (isNaN(offsetLeft) || isNaN(offsetTop)) {
fireTap = true;
}
if (fireTap) {
console.log('tap');
}
}
判斷方向
// 如果事件間隔小于300ms但是滑出屏幕,直接返回
if (dragDuration < 300 && dragState.currentLeft === undefined) return;
// 如果事件間隔小于300ms 或者 滑動位移超過屏幕寬度 1/2, 根據(jù)位移判斷方向
if (dragDuration < 300 || Math.abs(offsetLeft) > pageWidth / 2) {
towards = offsetLeft < 0 ? 'next' : 'prev';
}
// 如果非連續(xù),當(dāng)處于第一頁,不會出現(xiàn)上一頁,當(dāng)處于最后一頁,不會出現(xiàn)下一頁
if (!continuous) {
if ((index === 0 && towards === 'prev')
|| (index === pageCount - 1 && towards === 'next')) {
towards = null;
}
}
// 子頁面數(shù)量小于2時,不執(zhí)行滑動動畫
if (children.length < 2) {
towards = null;
}
執(zhí)行動畫
// 當(dāng)沒有options的時候,為自然滑動,也就是定時器滑動
function doAnimate(towards, options) {
if (children.length === 0) return;
if (!options && children.length < 2) return;
var prevPage, nextPage, currentPage, pageWidth, offsetLeft;
var pageCount = pages.length;
// 定時器滑動
if (!options) {
pageWidth = element.clientWidth;
currentPage = pages[index];
prevPage = pages[index - 1];
nextPage = pages[index + 1];
if (continuous && pages.length > 1) {
if (!prevPage) {
prevPage = pages[pages.length - 1];
}
if (!nextPage) {
nextPage = pages[0];
}
}
// 計算上一頁與下一頁之后
// 重置位移
// 參看doOnTouchMove
// 其實這里的options 傳與不傳也就是獲取上一頁信息與下一頁信息
if (prevPage) {
prevPage.style.display = 'block';
translate(prevPage, -pageWidth);
}
if (nextPage) {
nextPage.style.display = 'block';
translate(nextPage, pageWidth);
}
} else {
prevPage = options.prevPage;
currentPage = options.currentPage;
nextPage = options.nextPage;
pageWidth = options.pageWidth;
offsetLeft = options.offsetLeft;
}
var newIndex;
var oldPage = children[index];
// 得到滑動之后的新的索引
if (towards === 'prev') {
if (index > 0) {
newIndex = index - 1;
}
if (continuous && index === 0) {
newIndex = pageCount - 1;
}
} else if (towards === 'next') {
if (index < pageCount - 1) {
newIndex = index + 1;
}
if (continuous && index === pageCount - 1) {
newIndex = 0;
}
}
// 動畫完成之后的回調(diào)
var callback = function() {
// 得到滑動之后的激活頁面,添加激活class
// 重新賦值索引
if (newIndex !== undefined) {
var newPage = children[newIndex];
oldPage.classList.remove('is-active');
newPage.classList.add('is-active');
index = newIndex
}
if (isDone) {
end();
}
if (prevPage) {
prevPage.style.display = '';
}
if (nextPage) {
nextPage.style.display = '';
}
}
setTimeout(function() {
// 向后滑動
if (towards === 'next') {
isDone = true;
before(currentPage);
// 當(dāng)前頁執(zhí)行動畫,完成后執(zhí)行callback
translate(currentPage, -pageWidth, speed, callback);
if (nextPage) {
// 下一面移動視野中
translate(nextPage, 0, speed)
}
} else if (towards === 'prev') {
isDone = true;
before(currentPage);
translate(currentPage, pageWidth, speed, callback);
if (prevPage) {
translate(prevPage, 0, speed);
}
} else {
// 如果既不是左滑也不是右滑
isDone = true;
// 當(dāng)前頁面依舊處于視野中
// 上一頁和下一頁滑出
translate(currentPage, 0, speed, callback);
if (typeof offsetLeft !== 'undefined') {
if (prevPage && offsetLeft > 0) {
translate(prevPage, pageWidth * -1, speed);
}
if (nextPage && offsetLeft < 0) {
translate(nextPage, pageWidth, speed);
}
} else {
if (prevPage) {
translate(prevPage, pageWidth * -1, speed);
}
if (nextPage) {
translate(nextPage, pageWidth, speed);
}
}
}
}, 10);
}
后置工作:
清除一次滑動周期中保存的狀態(tài)信息
dragging = false;
dragState = {};
總結(jié)
整體來說實現(xiàn)原理還是比較簡單的,滑動開始記錄初始位置,計算上一頁與下一頁的應(yīng)該展示的頁面;滑動中計算位移,計算上一頁下一頁的位移;滑動結(jié)束根據(jù)位移結(jié)果執(zhí)行相應(yīng)的動畫。
有一個細(xì)節(jié)就是,在滑動中transition的效果置為空,是為了防止在滑動中上一頁與下一頁因為過渡存在而位移得不自然,在滑動結(jié)束后再給他們加上動畫效果。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
關(guān)于document.cookie的使用javascript
構(gòu)造通用的cookie處理函數(shù) cookie的處理過程比較復(fù)雜,并具有一定的相似性。因此可以定義幾個函數(shù)來完成cookie的通用操作,從而實現(xiàn)代碼的復(fù)用。2010-10-10
基于 Bootstrap Datetimepicker 聯(lián)動
這篇文章主要介紹了基于bootstrap datetimepicker 聯(lián)動效果,需要的朋友可以參考下2017-08-08
uniapp使用uni自帶websocket進行即時通訊詳細(xì)步驟
在開發(fā)程序過程中通信功能還是比較常用到的,下面這篇文章主要給大家介紹了關(guān)于uniapp使用uni自帶websocket進行即時通訊的詳細(xì)步驟,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-11-11
微信小程序?qū)崿F(xiàn)搜索功能并跳轉(zhuǎn)搜索結(jié)果頁面
這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)搜索功能并跳轉(zhuǎn)搜索結(jié)果頁面,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-05-05
JavaScript生成隨機數(shù)的4種自定義函數(shù)分享
這篇文章主要介紹了JavaScript生成隨機數(shù)的4種自定義函數(shù)分享,本文講解了4種方法并同時給出4個代碼片段,需要的朋友可以參考下2015-02-02

