其實經過vuejs很是容易實現spa中路由的過渡效果,網上也有很多教程。可是考慮一下如下需求javascript
好像也不是很簡單
最近幾個月天在仿DiDi應用寫一個web app,寫到「順風車」組件的時候發如今其組件下的兩個子組件:乘客組件以及車主組件,他們的切換方式正是知足上面3點要求。好幾番嘗試後終於寫出了知足要求的代碼,來和小夥伴分享~css
首先簡單的過渡效果我就很少作介紹了。經過vue
提供的transition
組件,咱們能夠很容易的實現一個普通的動畫效果。具體的能夠參考vuejs官方文檔。html
咱們先大體瞭解一下html結構vue
<div class="content-wrapper"> <!-- router-view指向組件driver和passenger --> <router-view class="content"></router-view> </div>
來實現第一個需求。
這個需求不難,分別給2個組件定義不一樣transition
動畫便可。
對於左邊的passenger組件,他是從左邊進入/離開的,相應的代碼爲transform: translateX(-100%)
,同理對於右邊的driver組件,相應代碼爲transform: translateX(100%)
。
特別注意的是,須要對router-view
添加一行css代碼(寫在.content
中)position: absolute
來使driver組件元素和passenger組件元素脫離文檔流,不然達不到2個組件同時出如今頁面中的效果。
效果以下:
上一個touch事件完成後,已滑動距離。實際在這個設計裏,由於咱們手指離開後,頁面不會停留在中間,不是滑過去切換路由,就是滑回去恢復原樣。因此currentDistance
並無什麼卵用,可是若是要即停即走,這個變量不可少。java
對於移動端,確定少不了手指滑動效果。在左右滑動頁面的時候,理應也能切換到對應的路由。滴滴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) } }
效果圖:
在拖動時,左右都是「白邊」,難看。怎麼處理?也不難。
咱們都寫過或者瞭解過輪播圖,其中一種寫法就是在第一張圖(元素)的左邊插入最後一張圖(元素),最後一張圖的右邊插入第一張。在拖動當前元素時,同時拖動其左/右的元素(也就是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>
到這裏好像就大功告成了?看一下效果:
等等,是你擼多了嗎?怎麼有兩個動畫?
實際上是由於路由切換後會自動觸發組件自己的transition,再加上本身寫的translate(this.leftEl, ...)
,就有2個啦。
知道緣由就很好處理了,去掉其中一個動畫便可。個人選擇是去掉組件自己的過渡效果。
具體作法就是給組件動態綁定transition
的name
屬性,來選擇性的使組件開啓/關閉過渡效果。同時對左右插入的元素監聽其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 } } } }
最終效果