移動端滾動問題(-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



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章