兼容問題:
如果元素定位在加了-webkit-overflow-scrolling:touch的容器上方時,如果上下滾動容器內容,定位的元素會跟着容器內容一起往上或往下滾動
iScroll是什麼
很多場景,如果使用body的滾動會很不方便,這時候,就會使用某個元素的局部滾動,噁心的事情就會發生了。
- pc端web,windows系統的瀏覽器會出現醜陋的滾動條。(其實也有最新的css樣式可以解決,兼容性欠佳)
- 手機移動端,ios瀏覽器無法慣性和彈性滑動(默認使用瀏覽器滾動,非ios系統原生滾動),如果加上
-webkit-overflow-scrolling: touch;
使用系統原生滾動,兼容性方面欠佳,bug不是一個兩個😭。 - 不利於實現一些個性化需求,例如加載,刷新,貼合滾動等。
巧了,iScroll解決了這些問題。
iScroll的作者是位國際友人,他的github飛機票在此。
遺憾的是,作者幾乎不再維護這個iScroll插件,網上的相關中文文檔很少,但是這並不影響我們拿着這個插件到處浪。
下面看看iScroll怎麼運作的
iScroll採用了css3的transform動畫模擬了慣性和彈性滾動的效果,效果和性能完美接近原生的滾動效果。同時提供了諸多功能包括自定義滾動條,指定滾動到元素
等功能,還可以輕鬆實現下拉刷新,上拉加載
。
iScroll的基本使用
首先肯定是先安裝了
npm install iscroll
yarn add iscroll
複製代碼
然後引用
import IScroll from 'iscroll/build/iscroll'; // 普通版
import IScroll from 'iscroll/build/iscroll-probe'; // 複雜版
import IScroll from 'iscroll/build/iscroll-infinite';
複製代碼
iscroll有幾中不同js文件,分別是普通本,複雜版,無限滾動版。這裏常用的是複雜版,是支持實時監聽滾動的位置的,如果不需要實時監聽,可以用普通版。
初始化使用
這裏以vue框架爲例
<template>
<div class="wrap">
<div class="scroll-area">
<div v-for="n in 50" class="item">{{ n }}</div>
</div>
</div>
</template>
<script>
import IScroll from 'iscroll/build/iscroll-probe';
export default {
data() {
scroll: null,
},
mounted() {
// 提示,因爲transform是對dom操作,所以需要在這個生命週期操作
this.scroll = new IScroll('.wrap', {
mouseWheel: true, // 允許鼠標滾輪
});
// 第一個參數是dom選擇器,建議使用唯一性的id,這裏以class爲例
// 第二個參數爲參數對象,是iscroll的一些配置
// 參數配置可以參考 http://wiki.jikexueyuan.com/project/iscroll-5/
}
}
</script>
<style>
.wrap{
height: 400px;
overflow: hidden;
/* 給滾動區域固定可滾動高度,並且超出隱藏 */
}
</style>
複製代碼
以上代碼就完成了簡單的iscroll初始化使用,可以看下效果
iScroll刷新
注意點,由於滾動內容可能是異步獲取並加載dom,如果不刷新iscroll,那麼滾動功能可能會受到影響,所以當異步內容加載後,需要調用刷新方法,刷新iscroll,刷新方式如下
<template>
<div ref="scroll" class="wrap">
<div class="scroll-area">
<div v-for="n in 50" class="item">{{ n }}</div>
</div>
</div>
</template>
<script>
import IScroll from 'iscroll/build/iscroll-probe';
export default {
data() {
scroll: null,
},
mounted() {
const el = this.$refs.scroll;
this.scroll = new IScroll('.wrap', {
...
});
// ① 異步數據刷新
getData().then(_=>{
this.scroll.refresh();
})
// ② 首次滑動時刷新
el.addEventListener('touchstart', _=>this.scroll.refresh());
}
}
</script>
複製代碼
監聽位置
this.scroll = new IScroll('.wrap', {
probeType: 3, // 滾動監聽級別 有3檔,3是像素級監聽
});
// 用iscroll實例註冊scroll事件
this.scroll.on('scroll', e => {
// 此處不用箭頭函數可以用this.x和this.y訪問實時位置,用了箭頭函數需要從實例上訪問
// this.scroll.x
// this.scroll.y
})
複製代碼
多的不說,看效果
注意取值的正負,監聽取得的值是transform的值,確認好正負值所對應的方向。
滾動到指定元素位置
這裏需要使用iscroll的貼合功能
this.scroll = new IScroll('.wrap', {
snap: '.item',
});
// 當設置snap屬性爲true時,iscroll會把容器可視區域分割爲一個page
// 當設置snap屬性爲元素選擇器時,iscroll會把對應的元素設置爲一個page
// 這裏我們設置爲'.item'
複製代碼
然後使用iscroll的goToPage
方法,跳到對應元素
this.scroll.goToPage(0, 30, 1000);
// 參數分別爲x, y, 動畫時間,
// 注意x,y是傳入索引,第一個是0,類推
複製代碼
也可以使用prev
和next
方法跳上一個或者下一個
this.scroll.prev();
this.scroll.next();
複製代碼
配置滾動條
如果想要滾動條呢,也很簡單
this.scroll = new IScroll('.wrap', {
scrollbars: true, // 開啓滾動條
shrinkScrollbars: 'scale', // 超出滾動時,縮放滾動條
});
複製代碼
/* 因爲iscroll的滾動條是定位實現,所以容器需要加一個相對定位 */
.wrap{
position: relative;
}
複製代碼
點擊事件
iscroll默認禁用了click事件,如果需要也可以開啓
this.scroll = new IScroll('.wrap', {
click: true,
});
複製代碼
並且iscroll很人性化的內置了tap事件,只要開啓tap,就可以在元素上響應tap
this.scroll = new IScroll('.wrap', {
tap: true,
});
複製代碼
<template>
<div ref="scroll" class="wrap">
<div class="scroll-area">
<div v-for="n in 50" class="item" @tap="onTap">{{ n }}</div>
</div>
</div>
</template>
複製代碼
iScroll的sticky
在基於dom元素的原生滾動中,是可以給內容添加position: sticky
來實現吸頂效果的。
吸頂: 在父元素的滾動過程中,如果子元素含有
position: sticky
和top: 0
樣式,那麼該內容滾到頂部時,會吸附在父元素的頂部,不會繼續向上滾動。(橫向滾動同理)
沒玩過position: sticky
的,去試下就知道啦。當然這個css並不是無敵的,原因就是兼容性不過關。戳這裏
好了,我們來說iscroll如何實現sticky,因爲iscroll使用了transform實現滾動,所以容器設置了overflow: hidden
,所以沒辦法用css的sticky實現,那麼既然是父元素transform滾動,那麼到達吸頂位置的時候,子元素反向transform是不是就可以了呢?
仔細看下面的代碼呢,很重要,認真看註釋️
// 這段代碼可以理解爲是對iscroll類的擴展
// 這裏的參數爲iscroll類
export const extendSticky = (iScroll) => {
let m = Math;
// 這裏是爲了兼容性配置的瀏覽器css前綴,網絡上有很多寫法呢
let vendor = (/webkit/i).test(navigator.appVersion) ? 'webkit' :
(/firefox/i).test(navigator.userAgent) ? 'Moz' :
'opera' in window ? 'O' : '',
has3d = 'WebKitCSSMatrix' in window && 'm11' in new WebKitCSSMatrix(),
trnOpen = 'translate' + (has3d ? '3d(' : '('),
trnClose = has3d ? ',0)' : ')';
/**
* 這裏開始拓展iscroll類
* @param selector 需要sticky的對象集合,包含元素和sticky的位置
* @return { iScrollStickyHeaders }
*/
// 在iscroll原型上添加 enableStickyHeaders 方法
iScroll.prototype.enableStickyHeaders = function (selector) {
return new iScrollStickyHeaders(this, selector); // 拓展方法採用新的類並傳參
};
// 參數,iscroll實例,需要sticky的元素集合
let iScrollStickyHeaders = function (iscroll, selector) {
if (!iscroll.options.useTransform) {
return;
}
this.iscroll = iscroll;
this.selector = selector;
this.initialize(); // 初始化
};
iScrollStickyHeaders.prototype = {
headers: [], // 存儲需要sticky的對象集合
initialize() {
let that = this;
this._augment();
this.iscroll.on('refresh', function() {
that._refresh() // 每次iscroll刷新,sticky方法也刷新
});
this.iscroll.refresh()
},
_refresh() { // 初始化或者刷新
let elms = this.selector;
this.headers = [ // 深拷貝對象集合
...elms,
]
// 此處對象集合的格式爲 { el: 元素, top: 需要sticky的位置 }
// 此處可以根據習慣和喜歡自行定義格式和邏輯代碼
this._translate(0, 0); // 初始化
},
_augment() { // 初始化函數
let that = this;
this.iscroll.on('scroll', function() {
that._translate(this.x, this.y) // iscroll滾動時,觸發主函數
});
this.iscroll.on('beforeScrollStart', function() {
that._translate(this.x, this.y) // iscroll即將滾動時,觸發主函數
});
this.iscroll.on('scrollStart', function() {
that._translate(this.x, this.y) // iscroll開始滾動時,觸發主函數
});
},
_translate(x, y) { // 主函數,到達sticky位置後,反向transform
let absY = m.abs(y); // 獲取y軸滾動的絕對值
this.headers.forEach((stickyObj) => { // 遍歷sticky對象
let translateY = 0; // sticky的反向transform默認爲0
let yy = m.abs(absY - stickyObj.el.offsetTop); // 計算iscroll的y軸滾動值-當前元素距離父級的值
// stickyObj.el.offsetTop爲固定值
// yy即爲當前元素距離容器頂部的位置
// absY < stickyObj.el.offsetTop說明該元素還沒到達頂部
// yy <= stickyObj.top 判斷元素是否到達需要sticky的位置
// ① 當元素還沒到達容器頂部時,默認爲0,再判斷是否到達指定sticky位置
// ② 如果沒到達指定sticky,依然爲0
// ③ 如果達到指定sticky位置,那麼就計算超過sticky位置後,需要反向transform的距離
// ④ 這裏默認指定位置是小於元素初始位置的,指定位置大於初始位置的,我想會很奇葩吧。
if (absY - stickyObj.el.offsetTop > 0 || yy <= stickyObj.top) {
// 這個公式需要反覆理解一下
// 當容器往上滾動時,容器的transform是負值,所以我們反向是正值
// 容器向上滾動值absY不斷變大,我們sticky就不斷向下transform
// stickyObj.el.offsetTop - stickyObj.top 即爲容器滾動多少範圍纔會讓元素到達指定sticky位置
// 計算iscroll容器的滾動值 - (初始位置 - 指定位置)
// 當滾動值等於初始位置和指定位置之差時,剛好等於0
// 隨着滾動值越來越大,超過0的部分,即爲需要反向transform的值
translateY = absY - (stickyObj.el.offsetTop - stickyObj.top);
} else {
translateY = 0;
}
// 最後拼接瀏覽器前綴,完成css賦值
stickyObj.el.style[vendor + 'Transform'] = trnOpen + ('0, ' + translateY + 'px') + trnClose;
});
},
};
};
export default extendSticky;
複製代碼
爲了便於理解,我就秀一下Axure的功力。
好了,上面的iscroll-sticky.js
工具已經完成,下面開始使用。
<template>
<div ref="scroll" class="wrap">
<div class="scroll-area">
<div v-for="n in 20" class="item">{{ n }}</div>
<div ref="sticky" class="sticky" :top="20">21</div>
<div v-for="n in 20" class="item">{{ n+20 }}</div>
</div>
</div>
</template>
<script>
import IScroll from 'iscroll/build/iscroll-probe';
import enableSticky from 'path/to/iscroll-sticky.js';
enableSticky(IScroll); // 這一步是將sticky方法掛載到iscroll原型上
export default {
data() {
scroll: null,
},
mounted() {
const el = this.$refs.scroll;
this.scroll = new IScroll('.wrap', {
...
});
const stickyEl = this.$refs.sticky;
// 允許元素對象集合sticky
this.scroll.enableStickyHeaders([
{
el: stickyEl,
top: stickyEl.getAttribute('top') // 此處我把top值配置在了原生prop
}
]);
}
}
</script>
複製代碼
看效果吧。
上面的
iscroll-sticky.js
是個靈活的js,可以根據自己的需求自行配置修改。
下拉刷新
事實上iscroll本身沒有下拉刷新功能,但是可以自己實現。
export default {
data() {
scroll: null,
status: 0, // 用一個變量記錄iscroll滾動狀態,默認爲0
txt: '下拉刷新', // 記錄刷新文本,默認
},
watch: {
status() {
// 每次iscroll的狀態碼變化時,就要刷新iscroll,以便iscroll重新計算dom元素
this.iscroll.refresh();
}
}
}
複製代碼
然後添加一個刷新文本(或者動畫)
<template>
<div ref="scroll" class="wrap">
<div class="scroll-area">
<div :class="{hide: status===0}" class="refresh">{{ txt }}</div>
<div v-for="n in 50" class="item">{{ n }}</div>
</div>
</div>
</template>
複製代碼
.refresh{
width: 100%;
height: 50px;
line-height: 50px;
text-align: center;
&.hide{
/* 當status爲0默認時,隱藏刷新文本,通過定位到容器外面 */
position: absolute;
left: 0;
top: -50px;
}
}
複製代碼
接下來監聽下拉iscroll
// ...
this.scroll.on('scroll', e => {
const y = this.iscroll.y; // 監聽下拉的y值,下拉是正值
if (y >= 50) { // 當下拉距離>=刷新文本高度時,
this.status = 1; // 狀態碼變爲1, 表示準備好刷新了
}
})
複製代碼
這個時候,status
的值變爲1,那麼之前被我們hide
的刷新文本,已經變爲正常的內容載入iscroll
了,這裏的dom變化需要理解清楚的,關鍵就在於狀態改變後iscroll的刷新,我們手指並沒有釋放,所以目前是準備刷新狀態,這個時候需要一個新的監聽,去監聽手指離開並且滾動停止。
this.scroll.on('scroll', e => {
const y = this.iscroll.y; // 監聽下拉的y值,下拉是正值
if (y >= 50) { // 當下拉距離>=刷新文本高度時,
this.txt = '釋放刷新';
this.status = 1; // 狀態碼變爲1, 表示準備好刷新了
} else if (y > 0) { // 如果返回了,又不想刷新了,恢復status爲0
this.txt = '下拉刷新';
this.status = 0;
}
})
this.scroll.on('scrollEnd', e => {
if (status === 1) { // 滾動停止時,如果是準備刷新狀態
this.txt = '刷新中。。。';
this.status = 2; // 改變狀態碼,開始刷新
this.scroll.disable(); // 刷新過程禁止滾動,這個禁用方法視需求而定。
this.updateData(); // 假設有一個更新數據的method
}
})
複製代碼
export default {
methods: {
updateData() {
getData().then(_=>{
// 數據更新完成
this.txt = '刷新完成';
// 延遲1秒後繼續隱藏刷新文本
setTimeout(_=>{
this.txt = '下拉刷新';
this.status = 0; // 狀態重置爲0
this.scroll.enable();
}, 1000);
})
}
}
}
複製代碼
看下demo的效果:
這裏附上一個我平時做的一個貓眼電影demo:
上拉加載
這個也是要自己實現,不過這個很簡單了,判斷滾動觸底即可。
this.scroll.on('scroll', e => {
// 此處scrollEl是容器高度,contentEl是內容高度,因爲y是負值,所以用scrollEl - contentEl
if (this.scroll.y <= scrollEl.offsetHeight - contentEl.offsetHeight) {
// do something 上拉加載
}
});
複製代碼
總結
iscroll是個很靈活的庫,可以根據自己想要的效果,自由配置。
如果對模塊化比較熟悉,可以嘗試將sticky
,下拉刷新
,上拉加載
封裝到一個組件中。