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

相關文章
相關標籤/搜索