近來看到 餓了麽 App和 h5站上,在商家詳情頁點餐之後,底部放置了一個點擊之後能夠彈出模態框查看點餐詳情的元素,其中有個背景遮罩層的漸進顯隱的效果。 憑著我少許的經驗,第一時間的想法是覺得這個遮罩層應該是使用 display:none;來控制隱藏和顯示的,但是這個屬性會破壞 transition ...
近來看到 餓了麽 App和 h5站上,在商家詳情頁點餐之後,底部放置了一個點擊之後能夠彈出模態框查看點餐詳情的元素,其中有個背景遮罩層的漸進顯隱的效果。
憑著我少許的經驗,第一時間的想法是覺得這個遮罩層應該是使用 display:none;來控制隱藏和顯示的,但是這個屬性會破壞 transition動畫,也就是說如果遮罩層是使用了這個屬性來控制顯示與隱藏,那麼漸進顯隱的效果似乎很難達到,效果應該是瞬間顯示與隱藏才對。
使用 Chrome 模擬移動端,查看了一下 餓了麽的實現方式,這才想到 餓了麽用到了 vue,此動畫效果其實是利用了 vue自帶的過渡動畫和鉤子函數實現的。
框架實現
基於 vue的動畫漸隱實現
利用框架實現這種效果真的是 so easy,不逼逼上代碼。
// HTML <div id="app"> <button class="btn" @click="show = !show">click</button> <transition name='fade'> <div class="box1" v-if="show"></div> </transition> </div> // CSS .box1 { width: 200px; height: 200px; background-color: green; } .fade-enter-active, .fade-leave-active { transition: opacity .5s } .fade-enter, .fade-leave-to{ opacity: 0; }
無圖無真相,看看效果助助興:
簡直不能更簡單
基於 react的動畫漸隱實現
react本身的單獨庫沒有自帶過渡動畫,不過有個 Animation Add-Ons: react-addons-css-transition-group
import React, {Component} from 'react' import ReactDOM from 'react-dom' import ReactCSSTransitionGroup from 'react-addons-css-transition-group' class TodoList extends React.Component { constructor(props) { super(props) this.state = { show: true } } render() { return ( <div> <button onClick={this.changeShow.bind(this)}>click</button> <ReactCSSTransitionGroup component="div" transitionName="fade" transitionEnterTimeout={500} transitionLeaveTimeout={300}> { this.state.show && <div className="box1"> </div> } </ReactCSSTransitionGroup> </div> ) } changeShow() { this.setState({ show: !this.state.show }) } }
樣式如下:
.box1 { width: 100px; height: 100px; background-color: green; transition: opacity .5s; } .fade-leave.fade-leave-active, .fade-enter { opacity: 0; } .fade-enter.fade-enter-active, .fade-leave { opacity: 1; }
依舊是很 easy
原生實現
以上都是框架實現,但如果項目歷史悠久,根本就沒用到這些亮瞎人眼的框架,充其量用了個 1.2版本的 jquery,那麼上面方法可都用不到了,我希望找到一種通用的原生方式,不利用任何框架。
visibility 代替 display
其中一種方案如題所示,因為 visibility這個屬性同樣能夠控制元素的顯隱,而且, visibility屬性在值 visible 與 hidden的來回切換中,不會破壞元素的 transition 動畫。
不過 visibility與 display 之間控制元素顯隱的最終效果還是有些差別的。
設置了 visibility:hidden; 的元素,視覺上確實是不可見了,但是元素仍然占據該占據的位置,仍然會存在於文檔流中,影響頁面的佈局,只不過設置了此屬性的元素在視覺上看不到,在頁面的原位置上留下一片空白而已(如果此元素具有寬高並且使用預設定位)。
而設置了 display:none;的元素,其既視覺上不可見,同時也不會占據空間,也就是說已經從文檔流中消失了。
visibility控制元素顯隱同樣是瞬時發生的,不過這種瞬時發生的情況又和 display的那種瞬時發生不太一樣, display是根本不會理會設置的 transition過渡屬性,設置了也和沒設置一樣。
但 visibility是有可能會理會這個值的,不過只理會 過渡時間 transition-duration這個屬性。
例如,從 visibility:hidden到 visibility:visible;變化時,如果設置了過渡時間為 3s,那麼在事件發生後,元素並不會立即呈現出從 hidden到 visible的效果,而是會像下圖那樣,先等待 3s,然後再瞬間隱藏,從顯示到最終消失視線中的時間確實 3s,只不過並不是逐漸過渡出現的。
上圖似乎有個問題,從顯示到隱藏確實是等待了 3s,但從隱藏到顯示,好像還是瞬間完成的,並沒有等待 3s的說法。
視覺上確實是這樣,不過這也只是視覺上的感覺而已,實際上這個等待時間真實存在的,只是看不到而已,另外,這裡的 等待也不是真的什麼都沒做純粹的 等待。
之所以 display會破壞 transition動畫,有個說法是,因為 transition對元素在整個過渡過程中的狀態控制,是根據元素過渡前後起始的狀態來計算得出的,例如從 opacity:0 到 opacity:1的變化,用時 3s,那麼 transition會計算在這 3s內的每一幀畫面中元素該有的 opacity值,從而完成過渡效果,其他的一些屬性,例如 width、 scale、 color等都可以轉化為數字進行計算 (說明文檔參見), 但 display是個尷尬的屬性,從 display:none到 display:block 該怎麼計算值呢?
計算不了,所以就只能 破壞了, visibility同樣如此,只不過 visibility比 display稍好一點,因為最起碼 visibility不會破罐子破摔,不會搞破壞。
從 visibility:hidden到 visibility:visible的過程中。因為沒辦法計算過渡階段沒幀的值,所以元素就直接顯示出來了,但內在的過渡操作依舊在元素顯示出來後顯示了 3s,而從 visibility:visible 到 visibility:hidden,元素在視覺上看起來等待的 3s內,實際在內部已經在進行 transition過渡操作,只不過還是因為沒辦法計算值,所以到了過渡階段的最後一刻時,就直接將元素設置為結束狀態,也就是隱藏了。
想要驗證這種說法,還需要配合另外一個屬性: opacity,此屬性也是配合 visibility完成過渡效果的搭配屬性。
實現代碼如下
// HTML <button class="btn">click</button> <div class="box1"></div>
// CSS .box1 { width: 200px; height: 200px; background-color: green; opacity: 0; visibility: hidden; transition: all 2s linear; } .show { opacity: .6; visibility: visible; }
js控制顯隱效果代碼如下:
let box1 = document.querySelector('.box1') let btn = document.querySelector('button') btn.addEventListener('click', ()=>{ let boxClassName = box1.className boxClassName.includes('show') ? box1.className = boxClassName.slice(0, boxClassName.length-5) : box1.className += ' show' })
效果依舊沒問題:
因為雖然 visibility沒辦法計算值,但 opacity可以,過渡的效果實際上是 opacity在起作用。
其實 opacity本身就能控制元素的顯隱,把上面代碼中的所有 visibility 全部刪除,效果依舊不變,並且和 visibility 一樣,設置了 opacity:0; 的元素依舊存在於文檔流中, but,相比於 visibility:hidden, opacity:0 的元素並不會出現點透。
而 visibility:hidden的元素就會出現點透,點擊事件會穿透 visibility:hidden的元素,被下麵的元素接收到,元素在隱藏的時候,就不會幹擾到其他元素的點擊事件。
關於這個說法,似乎網上有些爭論,但是我用迄今最新版的 Chrome Firefox 以及 360瀏覽器 進行測試, 都是上面的結果。
如果你只是想讓元素簡單的漸進顯隱,不用管顯隱元素會不會遮擋什麼點擊事件之類的,那麼完全可以不用加 visibility 屬性,加了反而是自找麻煩,但是如果需要考慮到這一點,那麼最好加上。
setTimeOut
如果不使用 visibility的話還好,但是如果使用了此屬性,那麼上述的解決方案其實還有點小瑕疵,因為 visibility從 IE10以及 Android4.4才開始支持,如果你需要支持這種版本的瀏覽器,那麼 visibility 就派不上用場了。
哎呦呦,公司網站最低要求都是 IE9,用不了了誒。
怎麼辦?再回到 display 這個屬性上。
為什麼 display 這個屬性會影響到 transition 動畫的原因上面已經大致說了下,既然問題是出在了 display上,那麼我可以同樣參考上面 visibility的做法,加個 opocity屬性進行輔助,又因為考慮到 display 比起 visibility 來說破壞性較大,所以再讓 opocity 與 display 分開執行不就行了嗎?
你如果寫成這種形式:
box1.style.display='block'
box1.style.opacity=1
其實還是沒用的,儘管 display值的設定在代碼上看起來好像是在 opacity前面,但是執行的時候卻是幾乎同時發生的。
我的理解是應該是瀏覽器對代碼進行了優化,瀏覽器看到你分兩步為同一個元素設置 CSS屬性,感覺有點浪費,為了更快地完成這兩步,它幫你合併了一下,放在一個 tick(參見 [ http://md.barretlee.com/(http://www.infoq.com/cn/articles/javascript-high-performance-animation-and-page-rendering ] )內執行,變成一步到位了,也就是同步執行了這兩句代碼。
那麼如何明確地讓瀏覽器不要合併到一個 tick內執行呢? setTimeOut就派上了用場。
setTimeOut 一個重要功能就是延遲執行,只要將 opacity屬性的設置延遲到 display後面執行就行了。
// CSS .box1 { width: 200px; height: 200px; background-color: green; display: none; opacity: 0; transition: all 2s linear; }
下麵是控制元素漸進顯示的代碼:
// JS let box1 = document.querySelector('.box1') let btn = document.querySelector('.btn') btn.addEventListener('click', ()=>{ let boxDisplay = box1.style.display if(boxDisplay === 'none') { box1.style.display='block' setTimeout(()=> { box1.style.opacity = 0.4 }) } })
上述代碼中,最關鍵的就是 setTimeOut 這一句,延遲元素 opacity屬性的設定。
setTiomeOut的第二個可選的時間 delay參數,我在最新版的 Chrome和 360 瀏覽器上測試,此參數可以不寫,也可以寫成 0或者其他數值,但是在 firefox上,此參數必須寫,不然漸進效果時靈時不靈,而且不能為 0,也不能太小,我測出來的最小數值是 14,這樣才能保證漸進效果,所以為了相容考慮,最好還是都統一加上時間。
至於為什麼是 14,我就不清楚了,不過記得以前看過一篇文章,其中說 CPU能夠反應過來的最低時間就是 14ms,我猜可能與這個有關吧。
顯示的效果有了,那麼要隱藏怎麼辦? setTimeOut 當然也可以,在 JS代碼的 if(boxDisplay==='none')後面再加個 else
else { box1.style.opacity = 0 setTimeout(()=>{ box1.style.display = 'none' }, 2000) }
隱藏時先設置 opacity,等 opacity過渡完了,再設置 display:none;。
但是這裡有點不太合理,因為雖然 setTimeOut的 delay參數 2000ms和 transition 時間 2s一樣大,但因為 JS是單線程,遵循時間輪詢,所以並不能保證 display屬性的設置剛好是在 opacity過渡完了的同時執行,可能會有更多一點的延遲,這取決於過渡動畫完成之刻, JS主線程是否繁忙。
當然,就算是延遲,一般也不會延遲多長時間的,人眼不太可能感覺得到,如果不那麼計較的話其實完全可以無視,但是如果我就吹毛求疵,要想做到更完美,那怎麼辦?
transitionend
transition 動畫結束的時候,對應著一個事件: transitionend,MDN [ https://developer.mozilla.org/en-US/docs/Web/Events/transitionend ] 上關於此事件的詳細如下:
transitionend 事件會在 CSS transition 結束後觸發. 當 transition完成前移除 transition時,比如移除 css的 transition-property 屬性,事件將不會被觸發,如在 transition完成前設置 display:none,事件同樣不會被觸發。
如果你能夠使用 transition,那麼基本上也就能夠使用這個事件了,只不過此事件需要加首碼的瀏覽器比較多(現在最新版的所有 主流瀏覽器,都已經不用寫首碼了),大致有如下寫法:
transitionend
webkitTransitionEnd
mozTransitionEnd
oTransitionEnd
使用此屬性,就可以避免上面 setTimeOut可能出現的問題了 ,使用示例如下:
// ... else { box1.style.opacity = 0 box1.addEventListener('transitionend', function(e) { box1.style.display = 'none' }); }
需要註意的是, transitionend 事件監聽的對象是所有 CSS中transition屬性指定的值,例如,如果你為元素設置了 transition:all3s;的 樣式,那麼元素可能無論是 left top還是 opacity 的改變,都會觸發該事件,也就是說此事件可能會被觸發多次,並且並不一定每次都是你想要觸發的,針對這種情況,最好加一個判斷。
既然是 涉及到了 JS實現的動畫,那麼其實可以考慮一下 把 setTimeout換成 requestAnimationFrame。
btn.addEventListener('click', ()=>{ let boxDisplay = box1.style.display if(boxDisplay === 'none') { box1.style.display='block' // setTimeOut 換成 requestAnimationFrame requestAnimationFrame(()=> { box1.style.opacity = 0.6 }) } else { box1.style.opacity = 0 box1.addEventListener('transitionend', function(e) { box1.style.display = 'none' }); } })
文章最開始說過的 vue 和 react這兩個框架實現示例動畫的方法,也利用到了這個 API,,監聽動畫過渡的狀態,為元素添加和刪除一系列過渡類名的操作,當然,並不是全部,此事件只能監聽動畫結束的這個時刻,其他時間點是無法監聽的。
- 以下為 transitionEnd 在 react-addons-css-transition-group源碼裡面出現的形式:
react-addons-css-transition-group對 transitionend做了相容,如果瀏覽器支持此屬性,則使用,如果不支持,就使用 setTimeOut這種形式。
- 以下為 transitionEnd 在 vue源碼裡面出現的形式:
另外,順帶一提的是,除了 transitionend 事件,還有一個 animationend [ https://developer.mozilla.org/en-US/docs/Web/Events/animationend ] 事件,此事件是對應 animation動畫, react-addons-css-transition-group 和 vue中也都對應著 transitionend 出現了此屬性的身影,這裡就不展開了。
如果你喜歡我們的文章,關註我們的公眾號和我們互動吧。
我們是轉轉FE團隊,歡迎大家關註公眾號 大轉轉FE 。更多的瞭解我們