IScroll那些事——內容不足時下拉刷新

以前項目中的列表是採用的IScroll,可是在使用IScroll有一個問題就是:當內容不足全屏的時候,是木有辦法往下拉的,這樣就達不到刷新的目的了。【這是本人工做中遇到的,具體例子具體分析,這裏只做一個參考javascript

大體的例子是這樣的:css

<style>
    * {
        margin: 0;
        padding: 0;
    }
    html,body,.container {
        width: 100%;
        height: 100%;
    }
    .container>ul>li {
        padding: 15px 20px;
        text-align: center;
        border-bottom: 1px solid #ccc;
    }
</style>

<div id="container" class="container">
    <ul class="scroller">
        <li>item1</li>
        <li>item2</li>
        <li>item3</li>
        <li>item4</li>
        <li>item5</li>
    </ul>
</div>

<script src="https://cdn.bootcss.com/iScroll/5.2.0/iscroll.min.js"></script>
<script>
    var myScroll = null;
    function onLoad() {
        myScroll = new IScroll('container');
    }
    window.addEventListener('DOMContentLoaded', onLoad, false);
</script>

那麼,既然超過一屏是能夠刷新的,那咱們就來逛逛代碼吧。在github上搜索iscroll,打開第一個,找到src下面的core.jshtml

1. 思路

首先既然要下拉,確定會觸發touchstarttouchmove以及touchend事件。搜索touchmove,很好,在_initEvents中的註冊了這個事件。java

_initEvents: function (remove) {
        // ...
        // 這裏省略若干代碼

        if ( utils.hasTouch && !this.options.disableTouch ) {
            eventType(this.wrapper, 'touchstart', this);
            eventType(target, 'touchmove', this);
            eventType(target, 'touchcancel', this);
            eventType(target, 'touchend', this);
        }

        // ...
},

好吧,看到這裏的時候,我表示懵了一下逼,這不就是個綁定事件麼?this又是一個什麼鬼,而後我去查了一下文檔,發現了這麼一個東西。文檔地址css3

target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);
target.addEventListener(type, listener[, useCapture, wantsUntrusted  ]); 
//  
// Gecko/Mozilla only

listener
    當所監聽的事件類型觸發時,會接收到一個事件通知(實現了 Event 接口的對象)對象。listener 必須是一個實現了 EventListener 接口的對象,或者是一個函數

木有看錯,listener是一個對象或者是一個函數。前提是這個對象實現了EventListener接口。咱們接着往下看,發現了這麼一個例子。git

var Something = function(element) {
    // |this| is a newly created object
    this.name = 'Something Good';
    this.handleEvent = function(event) {
        console.log(this.name); 
        // 'Something Good', as this is bound to newly created object
        switch(event.type) {
            case 'click':
                // some code here...
                break;
            case 'dblclick':
                // some code here...
                break;
        }
    };

    // Note that the listeners in this case are |this|, not this.handleEvent
    element.addEventListener('click', this, false);
    element.addEventListener('dblclick', this, false);

    // You can properly remove the listeners
    element.removeEventListener('click', this, false);
    element.removeEventListener('dblclick', this, false);
}
var s = new Something(document.body);

而後在去IScroll的源碼去找,發現了一樣的實現方式。在default文件夾中有一個handleEvent.jsgithub

好了,這個梗先告一段落。仍是繼續看源碼。在handleEvent.js中,有這麼一段東西。app

handleEvent: function (e) {
        switch ( e.type ) {
            case 'touchstart':
            case 'pointerdown':
            case 'MSPointerDown':
            case 'mousedown':
                this._start(e);
                break;
            case 'touchmove':
            case 'pointermove':
            case 'MSPointerMove':
            case 'mousemove':
                this._move(e);
                break;
            case 'touchend':
            case 'pointerup':
            case 'MSPointerUp':
            case 'mouseup':
            case 'touchcancel':
            case 'pointercancel':
            case 'MSPointerCancel':
            case 'mousecancel':
                this._end(e);
                break;
            // ...
        }
    }
};

發如今start/move/end分別調用了內部方法_start/_move/_end方法。去看看這三個方法,看其中可能會引發不會滑動的點。函數

_start方法中,看到這樣的幾行代碼,會不會是直接返回了呢?分析分析:測試

if ( !this.enabled || (this.initiated && utils.eventType[e.type] !== this.initiated) {
    return;
}

// ...

var point = e.touches ? e.touches[0] : e,
    pos;

this.initiated  = utils.eventType[e.type];
this.moved      = false;

initiated屬性在最開始確定是沒有的,而enabled默認是true,因此在最開始執行這個方法的時候是不會返回的,而是會給initiated這個屬性設置當前的eventType值,這個值會在_move方法中用到。重點來看看_move方法。

if ( !this.enabled || utils.eventType[e.type] !== this.initiated ) {
    return;
}

首先來進行類型判斷,由於在_start方法中已經定義了這個值,因此這裏也不會返回。接着往下看:

if ( timestamp - this.endTime > 300 && (absDistX < 10 && absDistY < 10) ) {
    return;
}

其實是兩次click事件的模擬】若是兩次滑動的時間大於了300ms,而且只要一個方向上的位移少於10像素,那麼也是會返回的。那麼會不會呢,打個斷點測試一下就知道了。這裏就不貼圖了,實際中的測試結果是,每一次移動確定是在300ms之內的,這裏之因此判斷300ms,主要是click事件執行會有一個300ms的延遲。而每一次移動,因爲手指的觸點比較大,仍是會大於10像素的,即便兩次不大於10像素,也是不影響的。因此這點不會返回。那麼繼續接着看:

// If you are scrolling in one direction lock the other
if ( !this.directionLocked && !this.options.freeScroll ) {
    if ( absDistX > absDistY + this.options.directionLockThreshold ) {
        this.directionLocked = 'h';     // lock horizontally
    } else if ( absDistY >= absDistX + this.options.directionLockThreshold ) {
        this.directionLocked = 'v';     // lock vertically
    } else {
        this.directionLocked = 'n';     // no lock
    }
}

if ( this.directionLocked == 'h' ) {
    if ( this.options.eventPassthrough == 'vertical' ) {
        e.preventDefault();
    } else if ( this.options.eventPassthrough == 'horizontal' ) {
        this.initiated = false;
        return;
    }

    deltaY = 0;
} else if ( this.directionLocked == 'v' ) {
    if ( this.options.eventPassthrough == 'horizontal' ) {
        e.preventDefault();
    } else if ( this.options.eventPassthrough == 'vertical' ) {
        this.initiated = false;
        return;
    }

    deltaX = 0;
}

第一個條件判斷只要是定義了此次滑動的方向是什麼。h表示水平方向,v表示豎直方向。咱們是要向下滑動,因此咱們關注的是豎直方向。看第二個條件判斷,若是是豎直方向,那麼將水平方向的deltaX值變爲0。這樣作的目的是保持絕對的豎直方向。由於移動實際仍是根據元素的位移值來的。當probe的版本爲2如下的時候,是根據css3的transform屬性來移動位移的,爲3版本的時候是根據決定對位來移動的。因此這裏只要不把咱們的deltaY置爲0就說明木有什麼問題。繼續往下看代碼:

deltaX = this.hasHorizontalScroll ? deltaX : 0;
deltaY = this.hasVerticalScroll ? deltaY : 0;

newX = this.x + deltaX;
newY = this.y + deltaY;
// ...

// 這裏是移動
this._translate(newX, newY);

測試中發現,這個hasVerticalScroll一直是false,那麼deltaY一直就是0,也就是移動了也白移動。找到問題緣由。那麼,這個hasVerticalScroll是從哪裏來的?全局找呀找,在refresh中找到這樣幾行代碼:

this.wrapperWidth   = this.wrapper.clientWidth;
this.wrapperHeight  = this.wrapper.clientHeight;

var rect = utils.getRect(this.scroller);
/* REPLACE START: refresh */

this.scrollerWidth  = rect.width;
this.scrollerHeight = rect.height;

this.maxScrollX     = this.wrapperWidth - this.scrollerWidth;
this.maxScrollY     = this.wrapperHeight - this.scrollerHeight;

/* REPLACE END: refresh */

this.hasHorizontalScroll    = this.options.scrollX && this.maxScrollX < 0;
this.hasVerticalScroll      = this.options.scrollY && this.maxScrollY < 0;

refresh方法會在IScroll實例化的時候調用一次。粗略一看,scrollY內置爲true,因此只有maxScrollY會大於0。往上看。this.wrapperHeight - this.scrollerHeight確定是大於0的呀,這就是問題所在。

那麼看看咱們最開始代碼,這裏的wrapperHeight爲文檔高度,scrollerHeight爲內容高度,因此wrapperHeight高度始終大於scrollHeight。可是,手機端頁面夾雜的列表,通常都有頭部、底部,而中間部分通常都會採用padding的形式來使得列表在全局滾動,這樣就不須要每次都要特定地計算列表的高度。

2. 解決方案

針對以上問題,只要咱們可以使內部的滾動部分高度大於容器高度,那麼就能觸發滾動。

2.1 粗略作法

能夠設置一個min-height屬性爲900px(900只是一個示例,只要夠大就能夠),這樣就能夠保證能夠滑動。

2.2 精準作法

計算當前的容器高度,而後比容器高度多一個像素便可。

相關文章
相關標籤/搜索