Vue2.0實現高仿餓了麼專案裡的小球飛入動畫
在學習Vue.js高仿餓了麼專案的過程中,有一個小球飛入購物車的動畫效果。專案是基於vue1.0的,如果是vue2.0的專案,該如何實現呢?自己也花時間研究了一會,從迷惑不解,各種嘗試未果,到後來咬文嚼字研讀vue 2.0官網關於過渡的章節,再到最終實現效果,心情十分愉悅,同時也算對vue2.0 transition 動畫也有所體會和掌握。記錄於此,分享大家!
先看下效果
在實現效果的過程中,我的體會有如下幾點:
1:多種transition過渡動畫,往往套路是內外兩層來層或多層來實現,每一層實現不同的transition;
2: 樣式部分,從不可見到可見,是enter 相關的樣式:.xx-enter,.xx-enter-to,.xx-enter-active;
從可見到不可見,是leave相關的樣式:.xx-leave, .xx-leave-to, .xx-leave-active;
其中..xx-enter/leave-active 寫的套路是:transition: all .5s linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n);等模式,引用官網的中文翻譯是:“這個類可以被用來定義進入過渡的過程時間,延遲和曲線函式
linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier等選項 是屬於CSS3 animation-timing-function 屬性的值,看到這裡隨便也把w3cschool裡幾個和動畫相關的屬性也一起學習了下:
比如:transition屬性是一個速記屬性有四個屬性:transition-property, transition-duration, transition-timing-function, and transition-delay
其中cubic-bezier 可以檢視下:http://cubic-bezier.com/ 從中拖拽自己想要的過渡曲線函式;
3:既然是過渡,一定有開始的狀態和結束的狀態,在vue.js中即可以通過transition 的js 鉤子函式寫類似 object.style.transform的方式 也可以通過vue.js自動新增的css樣式來規定,例如這裡的小球飛入動畫,不參考視訊裡的js的實現方式,用css來完成就是下面的寫法:
template結構:
<div class="ball-wrapper">
<transition-group name="drop" tag="div"><div class="ball" v-for="(ball,index) in balls" v-show="ball.show" :key="index">
<div class="inner inner-hook"></div>
</div>
</transition-group>
</div>
stylus樣式寫法:
.ball-wrapper .ball position fixed left 32px bottom 22px z-index 200 background-color red .inner width 15px height 15px border-radius 50% background-color #00A0DC transition all 1s linear &.drop-enter-active transition all 1s cubic-bezier(0.49, -0.29, 0.75, 0.41) &.drop-enter transform translate3d(0, -400px, 0) .inner transform translate3d(300px, 0, 0) &.drop-enter-to transform translate3d(0, 0, 0) .inner transform translate3d(0, 0, 0)上面程式碼中drop-enter規定了動畫開始的位置,把小球移動到 (x=300px,y=-400px)的位置;.drop-enter-to規定了動畫結束時的狀態,位置是回到自己的原點,然後水平使用 linear曲線1s內完成,垂直使用 曲線函式cubic-bezier(0.49, -0.29, 0.75, 0.41) 來完成;
4:wrapper和inner 兩層動畫實際上各自獨立進行,同時又因為父容器包裹inner ,所以inner的垂直變化受父容器的曲線函式影響,產生小球的拋物線效果,從background-color:red上 可以很明顯看出;
5:知道了這個原理又實際看到了效果,再對照視訊的做法,用js實現就很容易理解和實現了。
js部分的關鍵程式碼如下,這裡沒有考慮小球用完5個結束的情形
beforeEnter (el, done) { let count = this.balls.length; while (count--) { let ball = this.balls[count]; if (ball.show) { let rect = ball.el.getBoundingClientRect(); let x = rect.left - 32; let y = -(window.innerHeight - rect.top - 22); // el.style.display = ''; el.style.transform = `translate3d(0,${y}px,0`; el.style.webkitTransform = `translate3d(0,${y}px,0`; let inner = el.getElementsByClassName('inner-hook')[0]; inner.style.webkitTransform = `translate3d(${x}px,0,0)`; inner.style.transform = `translate3d(${x}px,0,0)`; // console.log(el); } } }, dropEnter (el, done) { /* eslint-disable no-unused-vars */ /* 觸發瀏覽器重繪; */ let rf = el.offsetHeight; this.$nextTick(() => { el.style.webkitTransform = 'translate3d(0, 0, 0)'; el.style.transform = 'translate3d(0, 0, 0)'; let inner = el.getElementsByClassName('inner-hook')[0]; inner.style.webkitTransform = 'translate3d(0, 0, 0)'; inner.style.transform = 'translate3d(0, 0, 0)'; el.addEventListener('transitionend', done); // done(); }); console.log(el); // done(); }6:dropEnter中 el.addEventListener('transitionend', done); 這句如果沒有,則不會觸發transition 的after-enter 事件,導致小球狀態不能被還原;但如果 是直接 done(); 則會看不到過渡的動畫效果;done做了什麼,未做研究,可能需要看vue.js的原始碼了!
7:觸發瀏覽器重繪 發現註釋也是沒有差別;因為重繪,可能需要等待DOM完全載入完成,所以這裡用到了this.$nextTick .
完整的程式碼:
<template> <div class="shopchart-wrapper"> <div class="content-left"> <div class="logo-wrapper"> <div class="logo" :class="{'hightlight':this.totalCount>0}"> <i class="icon-shopping_cart" :class="{'hightlight': this.totalCount > 0}"></i> </div> <div v-show="this.totalCount > 0" class="number">{{totalCount}}</div> </div> <div class="price" :class="{'highlight': this.totalPrice > 0}">¥{{totalPrice}}元</div> <div class="desc">另需配送費{{deliveryPrice}}元</div> </div> <div class="content-right"> <div class="pay" :class="payableStyle">{{paydesc}}</div> </div> <div class="ball-wrapper"> <transition-group name="drop" tag="div" v-on:before-enter="beforeEnter" v-on:enter="dropEnter" v-on:after-enter="afterEnter"> <div class="ball" v-for="(ball,index) in balls" v-show="ball.show" :key="index"> <div class="inner inner-hook"> </div> </div> </transition-group> </div> </div></template><script>export default { props: { 'selectedFoods': { type: Array, default () { return []; } }, 'delivery-price': { type: Number, default: 0 }, 'min-price': { type: Number, default: 0 } }, data () { return { balls: [ { show: false, el: null }, { show: false, el: null }, { show: false, el: null }, { show: false, el: null }, { show: false, el: null } ], droppedBalls: [] }; }, computed: { totalPrice () { let _totalPrice = 0.0; this.selectedFoods.forEach((f) => { // console.log(this.selectedFoods.length); _totalPrice += f.price * f.count; }); // console.log(_totalPrice); return _totalPrice; }, totalCount () { let _totalCount = 0; this.selectedFoods.forEach((f) => { _totalCount += f.count; }); return _totalCount; }, paydesc () { if (this.totalPrice === 0) { return `¥${this.minPrice}元起送`; } let _leftPrice = this.minPrice - this.totalPrice; if (this.totalPrice < this.minPrice) { return `還差¥${_leftPrice}元起送`; } else { return '去結算'; } }, payableStyle () { return { 'payable': this.totalPrice >= this.minPrice, 'not-enough': this.totalPrice > 0 && this.totalPrice < this.minPrice }; } }, methods: { dropMove (el) { for (var i = 0; i < this.balls.length; i++) { let b = this.balls[i]; if (!b.show) { b.show = true; b.el = el; this.droppedBalls.push(b); return; } } }, beforeEnter (el, done) { let count = this.balls.length; while (count--) { let ball = this.balls[count]; if (ball.show) { let rect = ball.el.getBoundingClientRect(); let x = rect.left - 32; let y = -(window.innerHeight - rect.top - 22); el.style.display = ''; el.style.transform = `translate3d(0,${y}px,0`; el.style.webkitTransform = `translate3d(0,${y}px,0`; let inner = el.getElementsByClassName('inner-hook')[0]; inner.style.webkitTransform = `translate3d(${x}px,0,0)`; inner.style.transform = `translate3d(${x}px,0,0)`; // console.log(el); } } }, dropEnter (el, done) { /* eslint-disable no-unused-vars */ /* 觸發瀏覽器重繪; */ let rf = el.offsetHeight; this.$nextTick(() => { el.style.webkitTransform = 'translate3d(0, 0, 0)'; el.style.transform = 'translate3d(0, 0, 0)'; let inner = el.getElementsByClassName('inner-hook')[0]; inner.style.webkitTransform = 'translate3d(0, 0, 0)'; inner.style.transform = 'translate3d(0, 0, 0)'; el.addEventListener('transitionend', done); // done(); }); // console.log(el); // done(); }, afterEnter (el) { el.style.display = 'none'; let ball = this.droppedBalls.shift(); ball.show = false; ball.el = null; console.log(el); } }};</script>
<!-- Add "scoped" attribute to limit CSS to this component only --><style lang='stylus' rel='stylesheet/stylus' scoped> .shopchart-wrapper position fixed left 0 bottom 0 z-index 10 height 48px width 100% display flex font-size 0 background-color #141d27 .content-left flex 1 .logo-wrapper display inline-block vertical-align top position relative top -10px margin 0 6px padding 6px width 56px height 56px box-sizing border-box border-radius 50% background #141d27 .logo width 100% height 100% border-radius 50% background #2b343c text-align center .icon-shopping_cart line-height 44px font-size 24px color #80858a &.hightlight color #fff &.hightlight background rgb(0,160, 220) .number position absolute; top 0 right 0 width 24px height 16px line-height 16px // margin-top -5px font-size 9px font-weight 700 color white text-align center border-radius 16px background-color rgb(240, 20, 20) box-shadow 0 4px 8px 0 rgba(0, 0, 0, 0.5) .price display inline-block vertical-align top padding-right 12px line-height 24px margin-top 12px border-right 1px solid rgba(255, 255, 255, 0.1) font-size 14px font-weight 700px color rgba(255, 255, 255, 0.4) &.highlight color #fff .desc display inline-block vertical-align top margin 12px 0 0 12px line-height 24px font-size 10px color rgba(255, 255, 255, 0.4) .content-right flex 0 0 105px width 105px .pay height 48px width 100% line-height 48px text-align center font-size 12px font-weight 700 color rgba(255, 255, 255, 0.4) background-color #2b333b &.payable color #fff background-color #00b43c &.not-enough color gray background-color #2b333b .ball-wrapper .ball position fixed left 32px bottom 22px z-index 200 // background-color red .inner width 15px height 15px border-radius 50% background-color #00A0DC transition all 1s linear &.drop-enter-active transition all 1s cubic-bezier(0.49, -0.29, 0.75, 0.41) // &.drop-enter // transform translate3d(0, -400px, 0) // .inner // transform translate3d(300px, 0, 0) // &.drop-enter-to // transform translate3d(0, 0, 0) // .inner // transform translate3d(0, 0, 0) // .inner // transform translate3d(0, 0, 0) // .inner // transform translate3d(300px, -400px, 0) // transform translate3d(300px, 0, 0)</style>