移動端滾動問題(-webkit-overflow-scrolling/iscroll)


兼容問題:

如果元素定位在加了-webkit-overflow-scrolling:touch的容器上方時,如果上下滾動容器內容,定位的元素會跟着容器內容一起往上或往下滾動

 

iScroll是什么

很多場景,如果使用body的滾動會很不方便,這時候,就會使用某個元素的局部滾動,惡心的事情就會發生了。

  1. pc端web,windows系統的瀏覽器會出現丑陋的滾動條。(其實也有最新的css樣式可以解決,兼容性欠佳)
  2. 手機移動端,ios瀏覽器無法慣性和彈性滑動(默認使用瀏覽器滾動,非ios系統原生滾動),如果加上-webkit-overflow-scrolling: touch;使用系統原生滾動,兼容性方面欠佳,bug不是一個兩個😭。
  3. 不利於實現一些個性化需求,例如加載,刷新,貼合滾動等。

巧了,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初始化使用,可以看下效果

 

效果1

 

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 }) 復制代碼

多的不說,看效果

 

效果2

 

注意取值的正負,監聽取得的值是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,類推 復制代碼

也可以使用prevnext方法跳上一個或者下一個

this.scroll.prev(); this.scroll.next(); 復制代碼

 

效果3

 

配置滾動條

如果想要滾動條呢,也很簡單

this.scroll = new IScroll('.wrap', { scrollbars: true, // 開啟滾動條 shrinkScrollbars: 'scale', // 超出滾動時,縮放滾動條 }); 復制代碼
/* 因為iscroll的滾動條是定位實現,所以容器需要加一個相對定位 */ .wrap{ position: relative; } 復制代碼

 

效果4

 

點擊事件

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: stickytop: 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的功力。

 

效果5

 

好了,上面的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> 復制代碼

看效果吧。

 

效果6

 

上面的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的效果:

效果7

 

這里附上一個我平時做的一個貓眼電影demo:

 

效果8

 

上拉加載

這個也是要自己實現,不過這個很簡單了,判斷滾動觸底即可。

this.scroll.on('scroll', e => { // 此處scrollEl是容器高度,contentEl是內容高度,因為y是負值,所以用scrollEl - contentEl if (this.scroll.y <= scrollEl.offsetHeight - contentEl.offsetHeight) { // do something 上拉加載 } }); 復制代碼

總結

iscroll是個很靈活的庫,可以根據自己想要的效果,自由配置。

如果對模塊化比較熟悉,可以嘗試將sticky,下拉刷新,上拉加載封裝到一個組件中。

 

better-scroll 

https://github.com/ustbhuangyi/better-scroll/issues/36

https://juejin.im/post/59300b2e2f301e006bcdd91c




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM