vuejs實現spa頁面組件滑動特效

vuejs實現spa頁面組件滑動特效

寫在前面的一些廢話

其實經過vuejs很是容易實現spa中路由的過渡效果,網上也有很多教程。可是考慮一下如下需求javascript

  1. 即將離開的組件和將要進入的組合同時出如今頁面中
  2. 用手指拖動頁面能夠切換路由,而不單單是點擊連接
  3. 結合以上兩點,拖動過程當中同時顯示兩個組件,手指離開屏幕後執行切換路由或者返回

好像也不是很簡單
最近幾個天在仿DiDi應用寫一個web app,寫到「順風車」組件的時候發如今其組件下的兩個子組件:乘客組件以及車主組件,他們的切換方式正是知足上面3點要求。好幾番嘗試後終於寫出了知足要求的代碼,來和小夥伴分享~css

我是正文

首先簡單的過渡效果我就很少作介紹了。經過vue提供的transition組件,咱們能夠很容易的實現一個普通的動畫效果。具體的能夠參考vuejs官方文檔html

Step 0

咱們先大體瞭解一下html結構vue

<div class="content-wrapper">
  <!-- router-view指向組件driver和passenger -->
  <router-view class="content"></router-view>
</div>

Step 1

來實現第一個需求。
這個需求不難,分別給2個組件定義不一樣transition動畫便可。
對於左邊的passenger組件,他是從左邊進入/離開的,相應的代碼爲transform: translateX(-100%),同理對於右邊的driver組件,相應代碼爲transform: translateX(100%)
特別注意的是,須要對router-view添加一行css代碼(寫在.content中)position: absolute來使driver組件元素和passenger組件元素脫離文檔流,不然達不到2個組件同時出如今頁面中的效果。
效果以下:
效果圖1
上一個touch事件完成後,已滑動距離。實際在這個設計裏,由於咱們手指離開後,頁面不會停留在中間,不是滑過去切換路由,就是滑回去恢復原樣。因此currentDistance並無什麼卵用,可是若是要即停即走,這個變量不可少。java

Step 2

對於移動端,確定少不了手指滑動效果。在左右滑動頁面的時候,理應也能切換到對應的路由。滴滴app就是這樣。如何滑動?實現思路就是以3個touch事件:touchstart,touchmove,touchend是核心,配合transform: translate來實現。額外要求:鬆手後判斷滑動距離,達到必定距即進行路由的切換,不然頁面「滑」回去。
如何讓元素跟着你的指尖走?咱們以driver組件爲例,先了解一下組件中各項數據web

data() {
  return {
    touch: {}, // 保存着起始位置x1和變化的位置x2
    touchStartTime: 0, // touch開始
    touchEndTime: 0, // touch結束時間
    currentDistance: 0 // 上一個touch事件完成後,已滑動距離。實際在這個設計裏,由於咱們手指離開後, 頁面不會停留在中間,不是滑過去切換路由,就是滑回去恢復原樣。因此這個變量並無什麼卵用,可是若是要*即停即走*,這個變量不可少。
    totalDiff: 0 // 總滑動距離
  }
},

首先,咱們在監聽元素的touchstart事件,在用戶touch頁面的時候記錄下位置信息,由於咱們是左右滑動,因此只關心x軸方向。回調函數以下segmentfault

function touchStart(ev) {
  let touch = ev.changedTouches[0]
  this.touch.x1 = touch.pageX // 本文中全部this指向vue組件實例
                                                          // 這裏是driver組件或是passenger
}

而後,也是重點,監聽touchmove事件。app

function touchMove(ev) {
  let touch = ev.changedTouches[0]
  this.touch.x2 = touch.pageX
  let diff = this.touch.x2 - this.touch.x1 // 差值,表示手指移動的距離
  this.totalDiff = diff + this.currentDistance // 總差值,表示手指移動的距離,正表示右滑,負左滑
  if (this.totalDiff < 0) { // driver組件是右滑,因此totalDiff不能小於0
    this.totalDiff = 0
  } else if (this.totalDiff > this.maxMoveDistance) { // 這裏maxMoveDistance爲屏幕寬度
    this.totalDiff = this.maxMoveDistance
  }
  let el = ev.currentTarget
  translate(el, this.totalDiff, 0) // 對組件進行滑動
  translate(this.leftEl, this.totalDiff, 0) // leftEl後面再作解釋
}

關於translate函數,具體實現以下:ide

/**
 * 簡單的移動函數
 * @param {HTML Object} el 目標節點
 * @param {number} x 水平方向的移動
 * @param {number} y 垂直方向的移動
 * @param {Object} options 可選參數
 * @param {Boolean} options.useTransfrom 是否經過transfrom來移動元素
 * @param {Boolean} options.transitionTimingFunction transition的timingFunction
 * @param {String} options.transitionDuration transition時間
 */
function translate(el, x, y, options) {
  const defaultOptions = {
    useTransfrom: true,
    transitionTimingFunction: 'cubic-bezier(0.165, 0.84, 0.44, 1)',
    transitionDuration: '0s'
  }
  for (let option in options) {
    defaultOptions[option] = options[option]
  }
  if (defaultOptions.useTransfrom) {
    el.style.transform = `translate3d(${x}px,${y}px,0)`
    el.style.transitionProperty = 'transform'
    el.style.transitionTimingFunction = defaultOptions.transitionTimingFunction
    el.style.transitionDuration = defaultOptions.transitionDuration
  } else {
    el.style.left = x
    el.style.top = y
  }
}

接下來就是touchend事件函數

function touchEnd(ev) {
  let touch = ev.changedTouches[0]
  this.touch.x2 = touch.pageX
  let diff = this.touch.x2 - this.touch.x1
  this.touchEndTime = Date.now()
  this.totalDiff = diff + this.currentDistance
  this.currentDistance = this.totalDiff
  let el = ev.currentTarget
  let touchTime = this.touchEndTime - this.touchStartTime
  // 當滑動距離超過一半或者快速滑動一段距離時,就進行完整的滑動,不然回彈
  // 快速滑動的數據是本身嘗試的,體驗可能不是很好^ ^
  if (this.totalDiff > this.maxMoveDistance / 2 || (touchTime < 150 && this.totalDiff > this.maxMoveDistance / 10)) {
    translate(el, this.maxMoveDistance, 0, {
      transitionTimingFunction: 'linear',
      transitionDuration: '.1s'
    })
    translate(this.leftEl, this.maxMoveDistance, 0, {
      transitionTimingFunction: 'linear',
      transitionDuration: '.1s'
    })
    this.$emit('dragedSlide') // 通知父組件進行路由切換
  } else {
    this.totalDiff = this.currentDistance = 0
    translate(el, this.totalDiff, 0)
    translate(this.leftEl, this.totalDiff, 0)
  }
}

效果圖:
效果圖2

Step 3

在拖動時,左右都是「白邊」,難看。怎麼處理?也不難。
咱們寫過或者瞭解過輪播圖,其中一種寫法就是在第一張圖(元素)的左邊插入最後一張圖(元素),最後一張圖的右邊插入第一張。在拖動當前元素時,同時拖動其左/右的元素(也就是Step 2中代碼裏的leftEl,固然還有righEl),來達到咱們要的效果。因此如今咱們的html結構是這樣的:

<div class="content-wrapper">
    <passenger class="out_of_screen out_of_screen-left"/>
    <!-- router-view指向組件driver和passenger -->
    <router-view class="content"/>
    <driver class="out_of_screen out_of_screen-right"/>
</div>

到這裏好像就大功告成了?看一下效果:
效果圖3
等等,是你擼多了嗎?怎麼有兩個動畫?
實際上是由於路由切換後會自動觸發組件自己的transition,再加上本身寫的translate(this.leftEl, ...),就有2個啦。
知道緣由就很好處理了,去掉其中一個動畫便可。個人選擇是去掉組件自己的過渡效果。
具體作法就是給組件動態綁定transitionname屬性,來選擇性的使組件開啓/關閉過渡效果。同時對左右插入的元素監聽其transitionend事件,配合上父組件的dragedSlide事件,來實現動態過渡,上代碼

<div class="content-wrapper">
  <passenger class="out_of_screen out_of_screen-left" @transitionend.native="updateRouter($event, 'passenger')/>
  <!-- router-view指向組件driver和passenger -->
  <router-view class="content" 
    @dragedSlide="confirmDragSlide"
    :transitionName="transitionName"
  />
  <driver class="out_of_screen out_of_screen-right" @transitionend.native="updateRouter($event, 'passenger')/>
</div>

父組件js部分

export default {
  beforeRouteUpdate(to, from, next) {
    this.transitionName = this.isDragedSlide ? '' : 'slide'
    next()
  },
 data() {
    return {
      transitionName: 'slide',
      isDragedSlide: false
    }
  },
  methods: {
        // ...
    // 是否經過手指拖動觸發滑屏
    confirmDragSlide() {
      this.isDragedSlide = true
    },
    updateRouter(ev, routeName) {
      if (this.isDragedSlide) {
        let el = ev.target
        this.$router.push(routeName)
        el.style.transform = ''
        el.style.transitionDuration = '0s'
        this.isDragedSlide = false
      }
    }
  }
}

最終效果
效果圖4


這裏有內容更新


End

相關文章
相關標籤/搜索