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

一、 基础实现

(1)功能

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

(2)实现

引入

  • better-scroll

props

  • probeType: better-scroll 配置项之一

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

  • refreshDelay: refresh事件的延迟时间

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

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

用于外部调用的方法

  • enable()

  • disable()

  • refresh()

  • 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>

二、问题归总

(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.js 移动端音乐app(一) 基础组件 scrollvue.js 移动端音乐app(一) 基础组件 scroll

问题分析

  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.js 移动端音乐app(一) 基础组件 scroll

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

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

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

因此,除了 touch 标志之外,还需一个 end 标志来确定 scroll 系列流程是否被 touch 行为提前打断。
1. 在 touchStart 中置 end 标志为 true
2. 在 scrollStart 中置 end 标志为 false
3. 在 scrollEnd 中置 end 标志为 true
4. 在 scrollEnd 中增加判断,如果 end 标志为 true,则不置 touch 标志为 false

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

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

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

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

三、完整项目地址

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

上一篇:如何在node环境中,使用ES6


下一篇:linux命令行下文件名中有空格的处理方法