之前項目中的列表是采用的IScroll
,但是在使用IScroll
有一個問題就是:當內容不足全屏的時候,是木有辦法往下拉的,這樣就達不到刷新的目的了。【這是本人工作中遇到的,具體例子具體分析,這里只作一個參考】
大致的例子是這樣的:
<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.js
。
1. 思路
首先既然要下拉,肯定會觸發touchstart
、touchmove
以及touchend
事件。搜索touchmove
,很好,在_initEvents
中的注冊了這個事件。
_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
又是一個什么鬼,然后我去查了一下文檔,發現了這么一個東西。文檔地址
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
接口。我們接着往下看,發現了這么一個例子。
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.js
。
好了,這個梗先告一段落。還是繼續看源碼。在handleEvent.js
中,有這么一段東西。
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 精准做法
計算當前的容器高度,然后比容器高度多一個像素即可。