前端必备自定义滚动库——iScroll

    |     2021年4月9日   |   html/css/js, web前端技术   |     0 条评论   |    1196

不少场景用 body 滚动不方便,只能局部滚动:Windows 浏览器滚动条难看,iOS 默认没有惯性和弹性,加上 -webkit-overflow-scrolling: touch 又一堆兼容问题,个性化的加载、刷新、贴合滚动更难做。iScroll 用 CSS3 transform 模拟接近原生的惯性/弹性滚动,还能自定义滚动条、滚到指定元素、下拉刷新和上拉加载。

作者是国际友人,GitHub 在此。项目几乎不再维护,中文文档也少,但不妨碍继续用。配置可参考 http://wiki.jikexueyuan.com/project/iscroll-5/。

一、基本使用

安装

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';

有普通版、复杂版(probe,可实时听位置)、无限滚动版。常用复杂版;不需要实时监听用普通版即可。

初始化(以 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>

transform 操作 DOM,所以在 mounted 里 new IScroll。第一个参数建议用唯一 id。效果如下:

16ad3de2282bdf6b

二、刷新

内容若异步插入 DOM,不 refresh 滚动会不准。异步完成或首次 touchstart 时调用 refresh:

<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
})

16ad3f8327f81fb1

取值正负对应 transform 方向,先确认坐标。箭头函数要从实例读 x/y,不用箭头可用 this.x / this.y。

四、滚动到指定元素(snap)

贴合功能:snap 为 true 时按可视区域分页;为选择器时每个匹配元素一页。

this.scroll = new IScroll('.wrap', {
  snap: '.item',
});
// 当设置snap属性为true时,iscroll会把容器可视区域分割为一个page
// 当设置snap属性为元素选择器时,iscroll会把对应的元素设置为一个page
// 这里我们设置为'.item'

再用 goToPage,参数是 x 索引、y 索引、动画时间(从 0 起):

this.scroll.goToPage(0, 30, 1000);

// 参数分别为x, y, 动画时间,

// 注意x,y是传入索引,第一个是0,类推

也可用 prev / next:

this.scroll.prev();

this.scroll.next();

16ad41861ceda9d4

五、配置滚动条

this.scroll = new IScroll('.wrap', {
  scrollbars: true, // 开启滚动条
  shrinkScrollbars: 'scale', // 超出滚动时,缩放滚动条
});
/* 因为iscroll的滚动条是定位实现,所以容器需要加一个相对定位 */
.wrap{
  position: relative;
}

滚动条是定位实现,容器要 position: relative。

16ad434618148e69

六、点击事件

默认禁用 click,需要就打开;也内置 tap:

this.scroll = new IScroll('.wrap', {

  click: true,

});
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>

七、Sticky 吸顶

原生滚动可用 position: sticky + top: 0 吸顶(横向同理)。兼容性一般,戳这里。iScroll 用 transform 且 overflow:hidden,CSS sticky 无效,到达吸顶位置时对子元素做反向 transform。

下面这段是对 iScroll 类的扩展,请对照注释看:

// 这段代码可以理解为是对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;

示意图:

20210409154749

工具文件 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>

╬в╨┼╜╪═╝_20210409154859

iscroll-sticky.js 可按需求改选择器和 top。

八、下拉刷新

库本身没有下拉刷新,要自己做。用 status 记录状态,变化时 refresh 好重新量 DOM:

export default {
  data() {
    scroll: null,
    status: 0, // 用一个变量记录iscroll滚动状态,默认为0
    txt: '下拉刷新', // 记录刷新文本,默认
  },
  watch: {
    status() {
      // 每次iscroll的状态码变化时,就要刷新iscroll,以便iscroll重新计算dom元素
      this.iscroll.refresh();
    }
  }
}

刷新文案默认藏在容器外(top: -50px),status 非 0 再进来:

<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;
  }
}

下拉 y 为正;超过文案高度把 status 置 1:

// ...
this.scroll.on('scroll', e => {
  const y = this.iscroll.y; // 监听下拉的y值,下拉是正值
  if (y >= 50) { // 当下拉距离>=刷新文本高度时,
    this.status = 1; // 状态码变为1, 表示准备好刷新了
  }
})

手指未放仍是准备刷新。再听 scroll 回弹和 scrollEnd 真正刷新(原文含「复制代码」分隔):

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:

╬в╨┼╜╪═╝_20210409154917

╬в╨┼╜╪═╝_20210409155004

效果8

九、上拉加载

判断触底即可(y 为负,用容器高减内容高):

this.scroll.on('scroll', e => {
  // 此处scrollEl是容器高度,contentEl是内容高度,因为y是负值,所以用scrollEl - contentEl
  if (this.scroll.y <= scrollEl.offsetHeight - contentEl.offsetHeight) {
    // do something 上拉加载
  }
});

十、结构约定与备注

库很灵活。熟悉模块化的话,可以把 sticky、下拉刷新、上拉加载封进一个组件。

转至:https://juejin.cn/post/6844903847941390349#heading-0

必须三层结构(滚动容器 → 数据容器 → 数据项),例如 div→ul→li 或三层 div。容器至少 overflow:hidden 和 position:relative。

<style>
    * {
        margin: 0;
    }
    .wrapper {
        width: 300px;
        height: 200px;
        overflow: hidden; /* 隐藏默认滚动条 */
        border: 1px solid #0094ff;
        margin: 0 auto;
        position: relative; /* 必须为 滚动条 父容器设置定位 */
    }
</style>
<!--1.三层结构-->
<div id="wrapper">
    <ul>
        <li>...</li>
        <li>...</li>
        ...
    </ul>
</div>
<!--2.初始化-->
<script type="text/javascript" src="iscroll.js"></script>
<script type="text/javascript">
    new IScroll('#wrapper', {
        mouseWheel: true, // 鼠标滚轮控制 
        scrollbars: true //滚动条的显示
    })
</script>
  • 可在 PC、移动设备和智能电视工作
  • 能处理滚动、缩放、平移、无限滚动、视差滚动、旋转等交互
能力 要点
初始化 三层 DOM + overflow:hidden + mounted 里 new IScroll
异步内容 refresh()
位置 probeType: 3 + on(‘scroll’)
贴合 snap + goToPage
吸顶 到达阈值后反向 transform
刷新/加载 自己听 y 与触底

一句话总结:iScroll 用 transform 模拟原生滚动;固定高度隐藏溢出,异步后 refresh,需要吸顶、刷新、加载时在 scroll 事件上自己算位移。

转载请注明来源:前端必备自定义滚动库——iScroll
本文链接地址:https://ai.zhousir.top/?p=3202
回复 取消