移動端效果之Swiper

寫在前面

最近在作移動端方面運用到了餓了麼的vue前端組件庫,由於不想單純用組件而使用它,故想深刻了解一下實現原理。後續將會繼續研究一下其餘的組件實現原理,有興趣的能夠關注下。javascript

swiper

代碼在這裏:戳我 or githubhtml

移動端效果之Picker前端

移動端效果之CellSwipervue

移動端效果之IndexListjava

1. 說明

父容器overflow:hidden;,子頁面transform:translateX(-100%);width:100%;android

2. 核心解析

2.1 頁面初始化

因爲全部頁面都在手機屏幕左側一個屏幕寬度的位置,所以最開始的狀況是頁面中看不到任何一個子頁面,因此第一步應該設置應該顯示的子頁面,默認狀況下defaultIndex:0git

function reInitPages() {
    // 得出頁面是否可以被滑動
    // 1. 子頁面只有一個
    // 2. 用戶手動設置不能滑動 noDragWhenSingle = true
    noDrag = children.length === 1 && noDragWhenSingle;

    var aPages = [];
    var intDefaultIndex = Math.floor(defaultIndex);
    var defaultIndex = (intDefaultIndex >= 0 && intDefaultIndex < children.length) 
        ? intDefaultIndex : 0;
    
    // 獲得當前被激活的子頁面索引
    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手機上,設置event.preventDefault()會起到必定的性能提高做用,使得滑動起來不是那麼卡。github

前置工做:性能

  • 若是用戶設置了 prevent:true, 滑動時阻止默認行爲
  • 若是用戶設置了stopPropagation:true, 滑動時阻止事件向上傳播
  • 若是動畫還沒有結束,阻止滑動
  • 設置dragging:true,滑動開始
  • 設置用戶滾動爲false

滑動開始:動畫

使用一個全局對象記錄信息,這些信息包括:

dragState = {
    startTime           // 開始時間
    startLeft           // 開始的X座標
    startTop            // 開始的Y座標(相對於整個頁面viewport pageY)
    startTopAbsolute    // 絕對Y座標(相對於文檔頂部 clientY)
    pageWidth           // 一個頁面寬度
    pageHeight          // 一個頁面的高度
    prevPage            // 上一個頁面
    dragPage            // 當前頁面
    nextPage            // 下一個頁面
};

2.3 容器滑動(onTouchMove)

套用全局dragState,記錄新的信息

dragState = {
    currentLeft         // 開始的X座標
    currentTop          // 開始的Y座標(相對於整個頁面viewport pageY)
    currentTopAbsolute  // 絕對Y座標(相對於文檔頂部 clientY)
};

那麼咱們就能夠經過開始和滑動中的信息來計算出一些東西:

1、滑動的水平位移(offsetLeft = currentLeft - startLeft

2、滑動的垂直位移(offsetTop = currentTopAbsolute - startTopAbsolute

3、是不是用戶的天然滾動,這裏的天然滾動說的是用戶並非想滑動swiper,而是想滑動頁面

```javascript
// 條件
// distanceX = Math.abs(offsetLeft);
// distanceY = Math.abs(offsetTop);
distanceX < 5 || ( distanceY >= 5 && distanceY >= 1.73 * distanceX )
```

4、判斷是左移仍是右移(offsetLeft < 0 左移,反之,右移)

5、重置位移

```javascript
// 若是存在上一個頁面而且是左移
if (dragState.prevPage && towards === 'prev') {
    // 重置上一個頁面的水平位移爲 offsetLeft - dragState.pageWidth
    // 因爲 offsetLeft 一直在變化,而且 >0
    // 那麼也就是說 offsetLeft - dragState.pageWidth 的值一直在變大,可是仍未負數
    // 這就是爲何當連續屬性存在的時候左滑會看到上一個頁面會跟着滑動的緣由
    // 這裏的 translate 方法其實很簡單,在滑動的時候去除了動畫效果`transition`,單純改變位移
    // 而在滑動結束的時候,加上`transition`,使得滑動到最後釋放的過渡更加天然
    translate(dragState.prevPage, offsetLeft - dragState.pageWidth);
} 

// 當前頁面跟着滑動
translate(dragState.dragPage, offsetLeft);

// 後一個頁面同理
if (dragState.nextPage && towards === 'next') {
    translate(dragState.nextPage, offsetLeft + dragState.pageWidth);
}
```

2.4 滑動結束(onTouchEnd)

前置工做:

在滑動中,咱們是能夠實時地來判斷究竟是不是用戶的天然滾動userScrolling,若是是用戶天然滾動,那麼swiper的滑動信息就不算數,所以要作一些清除操做:

dragging = false;
dragState = {};

固然若是userScrolling:false,那麼就是滑動子頁面,執行doOnTouchEnd方法

1、判斷是不是tap事件

```javascript
// 時間小於300ms,click事件延遲300ms觸發
// 水平位移和垂直位移棟小於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');
    }
}
```

2、判斷方向

```javascript
// 若是事件間隔小於300ms可是滑出屏幕,直接返回
if (dragDuration < 300 && dragState.currentLeft === undefined) return;

// 若是事件間隔小於300ms 或者 滑動位移超過屏幕寬度 1/2, 根據位移判斷方向
if (dragDuration < 300 || Math.abs(offsetLeft) > pageWidth / 2) {
    towards = offsetLeft < 0 ? 'next' : 'prev';
}

// 若是非連續,當處於第一頁,不會出現上一頁,當處於最後一頁,不會出現下一頁
if (!continuous) {
    if ((index === 0 && towards === 'prev') 
        || (index === pageCount - 1 && towards === 'next')) {
        towards = null;
    }
}

// 子頁面數量小於2時,不執行滑動動畫
if (children.length < 2) {
    towards = null;
}
```

3、執行動畫

```javascript
// 當沒有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;
        }
    }

    // 動畫完成以後的回調
    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);
            // 當前頁執行動畫,完成後執行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;
          // 當前頁面依舊處於視野中
          // 上一頁和下一頁滑出
          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);
}
```

​

後置工做:

清除一次滑動週期中保存的狀態信息

dragging = false;
dragState = {};

總結

總體來講實現原理仍是比較簡單的,滑動開始記錄初始位置,計算上一頁與下一頁的應該展現的頁面;滑動中計算位移,計算上一頁下一頁的位移;滑動結束根據位移結果執行相應的動畫。

有一個細節就是,在滑動中transition的效果置爲空,是爲了防止在滑動中上一頁與下一頁由於過渡存在而位移得不天然,在滑動結束後再給他們加上動畫效果。

相關文章
相關標籤/搜索