vue.js 移动端音乐app(一) 基础组件 scroll

1、 基础实现

(1)功能

对 better-scroll 插件的基本封装,实现移动端的滚动

(2)实现

引入

  • better-scrollcss

props

  • probeType: better-scroll 配置项之一vue

    (1)取值:
    1 滚动的时候会派发 scroll 事件,会截流。
    2 滚动的时候实时派发 scroll 事件,不会截流。
    3 除了实时派发 scroll 事件,在 swipe 的状况下仍然能实时派发 scroll 事件。
    (2)默认值:1
  • click: 点击事件是否生效git

  • refreshDelay: refresh事件的延迟时间github

  • listenScroll: 是否监听滚动事件,若是监听滚动事件,则父组件应当给自定义事件‘onscroll’绑定监听函数app

  • data: 用于控制 scroll 刷新从新计算高度的数据dom

用于外部调用的方法

  • enable()ecmascript

  • disable()异步

  • refresh()ide

  • scrollTo(x, y, time, [easing])函数

    easing取值只能为 swipe/swipeBounce/bounce
  • scrollToElement(el, time, [offsetX], [offsetY], [easing])

    offsetX,offsetY为number或true,true表示滚动到目标元素中心位置,数值则为设置滚动到目标元素的偏移量

思想步骤

  1. 在 mounted 钩子中,在 $nextTick() 的回调中初始化 scroll 实例。
    由于 scroll 实例初始化的时候必须保证其挂载对象(wrapper)的 DOM 已经渲染完成,因为 wrapper 中的数据可能异步获取的,所以必须放在 $nextTick() 中,获取更新数据后的 DOM,进行高度计算

  2. watch父组件传入的数据 data
    DOM 上的数据发生了变化,要获取更新后的 DOM ,在操做函数中一样要在$nextTick()的回调中进行 scroll 的刷新,refresh 从新计算高度。此处 setTimeout() 与 $nextTick() 做用相同。

代码

<template>
  <div ref="wrapper" @touchstart="onTouchstart">
    <slot>
    </slot>
  </div>
</template>

<style scoped lang="stylus" rel="stylesheet/stylus">

</style>

<script type="text/ecmascript-6">
  import BetterScroll from 'better-scroll'
  export default {
    name: 'scroll',
    props: {
      probeType: {
        type: Number,
        default: 1
      },
      click: {
        type: Boolean,
        default: true
      },
      data: {
        type: Array,
        default: null
      },
      refreshDelay: {
        type: Number,
        default: 20
      },
      listenScroll: {
        type: Boolean,
        default: false
      },
      listenScrollStart: {
        type: Boolean,
        default: false
      },
      listenScrollEnd: {
        type: Boolean,
        default: false
      },
      listenTouchStart: {
        type: Boolean,
        default: false
      },
      scrollX: {
        type: Boolean,
        default: true
      },
      scrollY: {
        type: Boolean,
        default: true
      }
    },
    mounted () {
      this.$nextTick(() => {
        this._initScroll()
      })
    },
    methods: {
      _initScroll () {
        if (!this.$refs.wrapper) {
          return
        }
        this.scroll = new BetterScroll(this.$refs.wrapper, {
          probeType: this.probeType,
          click: this.click,
          scrollX: this.scrollX,
          scrollY: this.scrollY
        })
        if (this.listenScroll) {
          let me = this
          this.scroll.on('scroll', (pos) => {
            me.$emit('onscroll', pos)
          })
        }
        if (this.listenScrollEnd) {
          let me = this
          this.scroll.on('scrollEnd', (pos) => {
            me.$emit('onscrollEnd', pos)
          })
        }
        if (this.listenScrollStart) {
          let me = this
          this.scroll.on('scrollStart', (pos) => {
            me.$emit('onscrollStart', pos)
          })
        }
      },
      // 存在自动滚动时(如歌词的自动播放)
      // 须要监听根据对 touch 事件的监听判断 scroll 过程是自动播放触发的仍是用户 touch 触发的
      onTouchstart (e) {
        if (!this.listenTouchStart) {
          return
        }
        this.$emit('ontouchStart', e)
      },
      disable () {
        this.scroll && this.scroll.disable()
      },
      enable () {
        this.scroll && this.scroll.enable()
      },
      refresh () {
        this.scroll && this.scroll.refresh()
      },
      scrollTo () {
        this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments)
      },
      scrollToElement () {
        this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments)
      }
    },
    watch: {
      data: {
        handler (newValue, oldValue) {
          setTimeout(() => {
            this.refresh()
          }, this.refreshDelay)
        },
        deep: true
      }
    }

  }
</script>

2、问题归总

(1)与父组件交互问题

  • 父组件中 scroll 下内容必须被包裹,不可出现以下结构。

<scroll>
    <div>
      ...
    </div>
    <div>
      ...
    </div>
</scroll>
  • 父组件对 srcoll 组件方法的调用、dom 的操做

<scroll ref="scrollName"> ... </scroll>
调用 scroll 中的方法:this.$refs.scrollName.methodName()
操做 dom(如改写style): this.$refs.scrollName.$el.style
  • 父组件引用 scroll 组件时 v-if 与 v-show 对其的影响

好比在 player.vue 组件中有以下结构。子组件 scroll 处在含有 v-show 属性来
控制显示的元素中。

1.v-if 与 v-show 的区别:v-if 会适当销毁和重建组件,且只有条件为真时才会进
行渲染。v-show 则在整个父组件建立时就渲染,只是根据条件改写元素的 css 属性
 display 的值来控制显示与否。
 
2.当 scroll 在 v-show 控制的元素中时,必须额外在显示条件为 true 时手动调用
 scroll.refresh() 刷新 scroll 从新计算其高度。
 
3.当 scroll 在 v-if 控制的元素中时,则无须手动刷新,由于 scroll 组件会被重
新建立,scroll 内部的 mounted 钩子的初始化及其对 data 的 watch 操做会自动
准确更新高度,实现滚动。

4.在 player.vue 中,因为全屏播放器和迷你播放器会被频繁切换,而初始化代价也
并非很大,因此使用 v-show 控制显示,另外 watch player.isFullpage 的值来
手动刷新 scroll 便可。
// 全屏显示的播放器
 <div class="normal-player" v-show="player.isFullpage">
    ...
    // 歌词部分,可滚动
    // lyricData是在组件mounted时后台获取的
    <scroll :data="lyricData">
        ...
    </scroll>
 </div>
// 迷你显示的播放器
 <div class="mini-player" v-show="!player.isFullpage">
    ...
 </div>

// js 部分 watch 代码
 watch: {
      'player.isFullpage': function (newFlag) {
        if (newFlag) {
          this.$nextTick(() => {
            this.$refs.lyricScroll.refresh()
          })
        },
       ...
    }

-父组件与 scroll 组件之间 touch 系列事件同时触发的问题

如在 player.vue 中,音乐播放器 CD 页面和歌词页是左右滑动切换显示的,封装成
了 fade-slider 组件来控制页面切换,在 fade-slider 中监听 touch 系列事件来
控制左右滑动,而scroll 组件在歌词页面中使用,监听 onscroll 事件控制歌词滑动
上下切换,scroll 与 fade-slier 是父子关系,所以直接绑定事件时,冒泡过程当中
两者的 touch 系列事件会同时被触发。为了实现需求,即页面左右滑动时 scroll 禁
止滚动,scroll 上下滚动时 fade-slider 也不要左右切换,必须作相应的处理。如
下代码:
// player.vue 组件片断
  <fade-slider>
    <div class="slider-item">
     ...
    </div>
    <div class="slider-item">
       ...
       // 监听 scroll 的滚动事件,此处主要是上下滚动
        <scroll @onscroll="onLyricScroll">
          ...
        </scroll>
    </div>
  </fade-slider>
// fade-slider 组件的 template 部分
<template>
  <div class="slider" ref="slider">
    <div @touchstart.capture="onTouchStart"
         @touchmove.capture="onTouchMove"
         @touchend="onTouchEnd">
      <slot>
      </slot>
    </div>
     ...
  </div>
</template>
1.要在歌词页面上下滑动歌词时,即在 scroll 上下滚动时,使歌词页面
(fade-slider组件的中一个页面)不要左右滑动,很简单,在 fade-slider 的
 touch 系列事件中对 touch 的位置和方向进行判断便可。

2.反过来,要在 fade-slider 控制歌词页面左右滑动时,使歌词页面中的 scroll 
不要上下滑动,由于它是封装出来的 onscroll 事件,不能直接对 touch 的位置和方
向进行判断,而另外去监听它的 touch 系列事件虽然也能够处理问题,但显然不合
适,不只逻辑重复,并且组件与 DOM 的耦合性也太高,不合适。

3.于是,当前问题就是要在父组件的 touch 过程当中,知足必定条件时去阻止子组件
的 scroll 事件的触发,显然在冒泡过程当中难以作到,所以解决方案:

(1)fade-slider组件(父组件)中捕获绑定 touch 系列事件:如 @touchstart.capture="onTouchStart"
(2)在 touch 系列事件处理过程当中,控制当肯定是左右滑动行为时,阻止 touch 系
列事件的传播:e.stopPropagation(),这样,scroll 中的滚动就不会被触发。

4.所以,总的逻辑就是:

(1)touch 系列事件第一时间由父组件捕获,进行 touch 行为的判断
(2)若是是左右滑动,则切换页面,同时阻止 touch 事件的进一步传递
(3)若是是上下滑动,则不作处理,使子组件的 touch 系列事件(scroll的内部)被触发,进行处理。

(2)自动滚动过程当中 touch 相关问题

需求分析

以下图:在歌词页面中,歌词即便用 scroll 组件,在音乐播放过程当中,歌词会自动播放,即根据当前音乐所对应的歌词,来 scrollToElement ,而在此过程当中,仍然接受 touch 行为,当由 touch 引发滚动时,暂停歌词的自动播放,并显示歌词控制条,同时根据滚动的距离高亮对应的歌词。歌词控制条分两部分:左侧显示当前滚动到的歌词对应的音乐的时间,右侧显示播放按钮,点击则直接播放此刻的音乐,歌词也随之从新定位

  • 图1:自动播放滚动时歌词控制条不显示,且高亮的歌词是当前音乐的进度对应的歌词

  • 图2:touch 引发滚动时,歌词暂停播放(音乐播放状依旧不变),歌词控制条显示,当前高亮歌词由当前滚动到的位置决定

vue-musicvue-music

问题分析

  1. 首先在滚动过程当中高亮的歌词以及歌词控制条上显示的对应的时间,显然是要经过 onscroll 判断,因此问题就在于如何在滚动过程当中合理有效的区分是自动播放的滚动仍是 touch 引发的滚动。

  2. 在确认是 touch 行为引发 scroll 滚动的前提下,大体要有三个阶段,作不一样的事情

    (1)scrollStart阶段:显示歌词控制条,中止歌词的自动滚动
    (2)onScroll阶段:不断根据当前滚动的偏移量更新高亮的歌词,以及对应的时间
    (3)scrollEnd阶段:滚动结束后,设置必定时间(如 1s)后,隐藏歌词控制条,恢复以前的播放状态
    (4)在以上阶段的任什么时候刻,一旦歌词控制条上的播放按钮被点击,都当即隐藏歌词控制条,并更新播放状态
  3. 总的来讲,核心内容涉及到 touchStart、scrollStart、onScroll、scrollEnd四个事件,重点是这些事件的触发顺序,以及滚动惯性的问题

问题解决

(一) 初步实现

(1)scroll 组件中已经绑定了并注册了 ontouchStart,onscrollStart,onscroll,onscrollEnd事件(代码见第一章),
在父组件中直接传入相应值并监听事件便可
(2)设置 touch标志,用来区分是不是自动滚动。在 touchStart 中
置其为 true,在 scrollEnd 置其为 false。之因此用 scrollEnd 做为结束时机而
不用 touchEnd 也是因为滚动惯性
(3)所以,自动滚动和 touch 滚动的处理流程分别以下图:

vue-music

(二) 惯性过程当中 touch 引发的 bug 修复

初步实现中的流程基本已经能够实现需求,touch 的标志已经能够控制区分自动
滚动和touch 滚动,可是会发现若是在 scroll 的惯性滚动中,再次 touch 屏幕,
则惯性滚动会中止,但 scroll 系列事件会再也不起做用,高亮的歌词与此时 touch 的
位置也不对应,即在其系列事件中 touch 的标志被置为 false 了,而这显然不是我
们想要的。
    touch 的标志之因此被置为了 false,是由 scrollEnd 的触发致使的。在惯性
滚动过程当中,touch 屏幕则会阻止惯性滚动,这是很明显的现象,据此想想,确定是 
touch 致使了 scrollEnd 的提早触发。即以下图:

vue-music

所以,除了 touch 标志以外,还需一个 end 标志来肯定 scroll 系列流程是否被 touch 行为提早打断。
1. 在 touchStart 中置 end 标志为 true
2. 在 scrollStart 中置 end 标志为 false
3. 在 scrollEnd 中置 end 标志为 true
4. 在 scrollEnd 中增长判断,若是 end 标志为 true,则不置 touch 标志为 false

vue-music

(三) touchStart、scrollStart、onscroll、scrollEnd 在 scroll 组件中注册的区别

  1. scrollStart、onscroll、scrollEnd 均是 better-scroll 中注册的事件,使用时在 better-scroll 对象(new BetterScroll())上 .on(事件名,处理函数) 监听便可

  2. touchStart 是原生事件,在 scroll 组件中绑定在最外层元素上

3、完整项目地址

Github: https://github.com/aphasic/mu...

相关文章
相关标签/搜索