其实经过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 } } } }
最终效果