Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BetterScroll:在移动端滚动场景的应用 #22

Open
AmyFoxFN opened this issue Sep 11, 2017 · 2 comments
Open

BetterScroll:在移动端滚动场景的应用 #22

AmyFoxFN opened this issue Sep 11, 2017 · 2 comments

Comments

@AmyFoxFN
Copy link

AmyFoxFN commented Sep 11, 2017

BetterScroll 是一款重点解决移动端各种滚动场景需求的开源插件(GitHub地址),适用于滚动列表、选择器、轮播图、索引列表、开屏引导等应用场景。

为了满足这些场景,它不仅支持惯性滚动、边界回弹、滚动条淡入淡出等效果的灵活配置,让滚动更加流畅,同时还提供了很多 API 方法和事件,以便我们更快地实现滚动场景下的需求,如下拉刷新、上拉加载。

由于它基于原生 JavaScript 实现,不依赖任何框架,所以既可以原生 JavaScript 引用,也可以与目前前端 MVVM 框架结合使用,比如,其官网上的示例就是与 Vue 的结合。

首先,让我们来看一下它是怎样让滚动更流畅的吧。

让滚动更流畅

在移动端,如果你使用过 overflow: scroll 生成一个滚动容器,会发现它的滚动是比较卡顿,呆滞的。为什么会出现这种情况呢?

因为我们早已习惯了目前的主流操作系统和浏览器视窗的滚动体验,比如滚动到边缘会有回弹,手指停止滑动以后还会按惯性继续滚动一会,手指快速滑动时页面也会快速滚动。而这种原生滚动容器却没有,就会让人感到卡顿。

BetterScroll 的滚动体验

试一试 BetterScroll 的滚动体验吧。体验地址

可以发现,在增加惯性滚动,边缘回弹等效果之后,明显流畅、舒服了很多。那么,这些效果是怎么实现的呢?

惯性滚动

BetterScroll 在用户滑动操作结束时,还会继续惯性滚动一段。首先看一下源码中的 BScroll.prototype._end 函数,这是 touchend、mouseup、touchcancel、mousecancel 事件的处理函数,也就是用户滚动操作结束时的逻辑。

BScroll.prototype._end = function (e) {
	...
	if (this.options.momentum && duration < this.options.momentumLimitTime && (absDistY > this.options.momentumLimitDistance || absDistX > this.options.momentumLimitDistance)) {
      let momentumX = this.hasHorizontalScroll ? momentum(this.x, this.startX, duration, this.maxScrollX, this.options.bounce ? this.wrapperWidth : 0, this.options)
        : {destination: newX, duration: 0}
      let momentumY = this.hasVerticalScroll ? momentum(this.y, this.startY, duration, this.maxScrollY, this.options.bounce ? this.wrapperHeight : 0, this.options)
        : {destination: newY, duration: 0}
      newX = momentumX.destination
      newY = momentumY.destination
      time = Math.max(momentumX.duration, momentumY.duration)
      this.isInTransition = 1
    }
	...
}

以上代码的作用是,在用户滑动操作结束时,如果需要开启了惯性滚动,用 momentum 函数计算惯性滚动距离和时间。该函数,根据用户滑动操作的速度和 deceleration选项 ——惯性减速来计算滚动距离,至于滚动时间,也是一个可配置的选项。

function momentum(current, start, time, lowerMargin, wrapperSize, options) {  
  ...
  let distance = current - start
  let speed = Math.abs(distance) / time
  ...
  let duration = swipeTime
  let destination = current + speed / deceleration * (distance < 0 ? -1 : 1)
  ...
}

边缘回弹

超过边缘时的回弹,有两个处理步骤,第一步是滚动到超过边界时速度要变慢,第二步是回弹到边界。其中,第一步是在源码的 BScroll.prototype._move 函数,这是 touchmove 和 mousemove 事件的处理函数,也就是在用户滑动操作过程中的逻辑。

// Slow down or stop if outside of the boundaries
if (newY > 0 || newY < this.maxScrollY) {
	if (this.options.bounce) {
        newY = this.y + deltaY / 3
    } else {
        newY = newY > 0 ? 0 : this.maxScrollY
    }
}

第二步是调用BScroll.prototype.resetPosition函数,回弹到边界。

BScroll.prototype.resetPosition = function (time = 0, easeing = ease.bounce) {
    ...
    let y = this.y
    if (!this.hasVerticalScroll || y > 0) {
      y = 0
    } else if (y < this.maxScrollY) {
      y = this.maxScrollY
    }
	...
    this.scrollTo(x, y, time, easeing)
    ...
  }

流畅的滚动仅仅是基础,BetterScoll 真正的能力在于:提供了大量通用 / 定制的 option 选项API 方法事件,让各种滚动需求实现起来更高效。

如何应用于各种需求场景

下面,以结合 Vue 的使用为例,说一下 BetterScroll 在各种场景下的姿势。

普通滚动列表

比如,有如下列表:

<div ref="wrapper" class="list-wrapper">
  <ul class="list-content">
    <li @click="clickItem($event,item)" class="list-item" v-for="item in data">{{item}}</li>
  </ul>
</div>

我们想要让它垂直滚动,只需要对该容器进行简单的初始化。

import BScroll from 'better-scroll'

const options = {
  scrollY: true // 因为scrollY默认为true,其实可以省略
}

this.scroll = new BScroll(this.$refs.wrapper, options)

对于 Vue 中使用 BetterScroll,有一个需要注意的点是,因为在 Vue 模板中列表渲染还没完成时,是没有生成列表 DOM 元素的,所以需要在确保列表渲染完成以后,才能创建 BScroll 实例,因此在 Vue 中,初始化 BScroll 的最佳时机是 mouted 的 nextTick。

// 在 Vue 中,保证列表渲染完成时,初始化 BScroll
mounted() {
   setTimeout(() => {
     this.scroll = new BScroll(this.$refs.wrapper, options)
   }, 20)
},

初始化之后,这个 wrapper 容器就能够优雅地滚动了,并且可以通过 BScroll 实例this.scroll使用其提供的 API 方法和事件。

下面介绍几个常用的选项、方法和事件。

滚动条

scrollbar选项,用来配置滚动条,默认为 false。当设置为 true 或者是一个 Object,开启滚动条。还可以通过 fade 属性,配置滚动条是随着滚动操作淡入淡出,还是一直显示。

// fade 默认为 true,滚动条淡入淡出
options.scrollbar = true

// 滚动条一直显示
options.scrollbar = {
  fade: false
}

this.scroll = new BScroll(this.$refs.wrapper, options)

具体效果可见普通滚动列表-示例

下拉刷新

pullDownRefresh选项,用来配置下拉刷新功能。当设置为 true 或者是一个 Object 的时候,开启下拉刷新,可以配置顶部下拉的距离(threshold)来决定刷新时机,以及回弹停留的距离(stop)

options.pullDownRefresh = {
  threshold: 50, // 当下拉到超过顶部 50px 时,触发 pullingDown 事件
  stop: 20 // 刷新数据的过程中,回弹停留在距离顶部还有 20px 的位置
}

this.scroll = new BScroll(this.$refs.wrapper, options)

监听 pullingDown 事件,刷新数据。并在刷新数据完成之后,调用 finishPullDown() 方法,回弹到顶部边界

this.scroll.on('pullingDown', () => {
  // 刷新数据的过程中,回弹停留在距离顶部还有20px的位置
  RefreshData()
    .then((newData) => {
      this.data = newData
      // 在刷新数据完成之后,调用 finishPullDown 方法,回弹到顶部
      this.scroll.finishPullDown()
  })
})

具体效果可见普通滚动列表-示例

上拉加载

pullUpLoad选项,用来配置上拉加载功能。当设置为 true 或者是一个 Object 的时候,可以开启上拉加载,可以配置离底部距离阈值(threshold)来决定开始加载的时机

options.pullUpLoad = {
  threshold: -20 // 在上拉到超过底部 20px 时,触发 pullingUp 事件
}

this.scroll = new BScroll(this.$refs.wrapper, options)

监听 pullingUp 事件,加载新数据。

this.scroll.on('pullingDown', () => {
  loadData()
    .then((newData) => {
      this.data.push(newData)
  })
})

具体效果可见普通滚动列表-示例

选择器

wheel选项,用于开启并配置选择器。可配置选择器当前选择的索引(selectedIndex),列表的弯曲弧度(rotate),以及切换选择项的调整时间(adjustTime)。

options.wheel = {
  selectedIndex: 0,
  rotate: 25,
  adjustTime: 400
}

// 初始化选择器的每一列
this.wheels[i] = new BScroll(wheelWrapper.children[i], options)

具体效果可见选择器 - 示例

其中联动选择器,需要监听每个选择列表的选择,来改变其他选择列表。

data() {
   return {
     tempIndex: [0, 0, 0]
   }
},
...
// 监听每个选择列表的选择
this.wheels[i].on('scrollEnd', () => {
  this.tempIndex.splice(i, 1, this.wheels[i].getSelectedIndex())
})
...
// 根据当前选择项,确定其他选择列表的内容
computed: {
  linkageData() {
    const provinces = provinceList
    const cities = cityList[provinces[this.tempIndex[0]].value]
    const areas = areaList[cities[this.tempIndex[1]].value]

    return [provinces, cities, areas]
  }
},

具体效果可见选择器 - 示例中的联动选择器。

轮播图

snap选项,用于开启并配置轮播图。可配置轮播图是否循环播放(loop),每页的宽度(stepX)和高度(stepY),切换阈值(threshold),以及切换速度(speed)。

options = {
  scrollX: true,
  snap: {
    loop: true, // 开启循环播放
    stepX: 200, // 每页宽度为 200px
    stepY: 100, // 每页高度为 100px
    threshold: 0.3, // 滚动距离超过宽度/高度的 30% 时切换图片
    speed: 400 // 切换动画时长 400ms
  }
}

this.slide = BScroll(this.$refs.slide, options)

具体效果可见轮播图 - 示例

特殊场景

除了普通滚动列表、选择器、轮播图等基础滚动场景,还可以利用 BetterScroll 提供的能力,做一些特殊场景。

索引列表

索引列表,首先需要在滚动过程中实时监听滚动到哪个索引的区域了,来更新当前索引。在这种场景下,我们可以使用probeType选项,当此选项设置为 3 时,会在整个滚动过程中实时派发 scroll 事件。从而获取滚动过程中的位置。

options.probeType = 3
this.scroll = new BScroll(this.$refs.wrapper, options)

this.scroll.on('scroll', (pos) => {
  const y = pos.y

  for (let i = 0; i < listHeight.length - 1; i++) {
    let height1 = listHeight[i]
    let height2 = listHeight[i + 1]
    if (-y >= height1 && -y < height2) {
	  this.currentIndex = i
    }
  }
})

当点击索引时,使用scrollToElement()方法滚动到该索引区域。

scrollTo(index) {
  this.$refs.indexList.scrollToElement(this.$refs.listGroup[index], 0)
}

具体效果可见索引列表 - 示例

开屏引导

开屏引导,其实就是一种不自动循环播放的横向滚动轮播图而已。

options = {
  scrollX: true,
  snap: {
    loop: false
  }
}

this.slide = BScroll(this.$refs.slide, options)

具体效果可见开屏引导 - 示例。因为此需求场景一般只有移动端才有,所以最好在手机模式下看效果。

自由滚动

freeScroll选项,用于开启自由滚动,允许横向和纵向同时滚动,而不限制在某个方向。

options.freeScroll = true

另外需要注意的是,此选项在 eventPassthrough 设置了保持原生滚动时无效。

具体效果可见自由滚动-示例

小结

BetterScroll 可以用于几乎所有滚动场景,本文仅介绍了在一些典型场景下的使用姿势。

作为一款旨在解决移动端滚动需求的插件,BetterScroll 开放的众多选项、方法和事件,其实,就是提供了一种让我们更加快捷、灵活、精准时机地处理滚动的能力。

@Meqn
Copy link

Meqn commented Dec 20, 2017

nice

@proc07
Copy link

proc07 commented Dec 21, 2017

你好,我在阅读源码中有个困惑,不知你对源码( start\move\end )方法中使用了这个判断 this.initiated !== _eventType 是怎么理解的呢? 在 start 方法中赋值了 this.initiated = _eventType ,那为什么会不等于?难道是页面在 PC 与 Mobile 中进行切换的吗? (PS: 文章写的很棒)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants