做者真的很懶,這篇文章從想法開始到如今都幾個月了。😊javascript
不少場景,若是使用body的滾動會很不方便,這時候,就會使用某個元素的局部滾動,噁心的事情就會發生了。css
-webkit-overflow-scrolling: touch;
使用系統原生滾動,兼容性方面欠佳,bug不是一個兩個😭。巧了,iScroll解決了這些問題。html
iScroll的做者是位國際友人,他的github飛機票在此。vue
遺憾的是,做者幾乎再也不維護這個iScroll插件,網上的相關中文文檔不多,可是這並不影響咱們拿着這個插件處處浪。java
下面看看iScroll怎麼運做的ios
iScroll採用了css3的transform動畫模擬了慣性和彈性滾動的效果,效果和性能完美接近原生的滾動效果。同時提供了諸多功能包括自定義滾動條,指定滾動到元素
等功能,還能夠輕鬆實現下拉刷新,上拉加載
。css3
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文件,分別是普通本,複雜版,無限滾動版。這裏經常使用的是複雜版,是支持實時監聽滾動的位置的,若是不須要實時監聽,能夠用普通版。git
這裏以vue框架爲例github
<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初始化使用,能夠看下效果web
注意點,因爲滾動內容多是異步獲取並加載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>
複製代碼
在基於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
,下拉刷新
,上拉加載
封裝到一個組件中。
評論有人提到
better-scroll
,沒什麼問題,喜歡哪一個用哪一個。
歡迎點贊收藏,後續和iscroll相關的會及時更新進來。