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