元素顯示與隱藏時的 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: 200 px;
background-color: green;
}
.fade-enter-active, .fade-leave-active {
transition: opacity .5s
}
.fade-enter, .fade-leave-to{
opacity: 0;
}
無圖無真相,看看效果助助興:
簡直不能更簡單
- 基於
react
的動畫漸隱實現
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
的說法。
視覺上確實是這樣,不過這也只是視覺上的感覺而已,實際上這個等待時間真實存在的,只是看不到而已。
想要驗證這種說法,還需要配合另外一個屬性: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'
})
效果依舊沒問題:
其實 opacity
本身就能控制元素的顯隱,把上面程式碼中的所有 visibility
全部刪除,效果依舊不變。
opacity
確實能夠讓元素在視覺上顯示和隱藏,並且和 visibility
一樣,設定了 opacity:0;
的元素依舊存在於文件流中,but
,相比於 visibility: hidden
, opacity: 0
的元素並不會出現點透。
而 visibility: hidden
的元素就會出現點透,點選事件會穿透 visibility: hidden
的元素,被下面的元素接收到,元素在隱藏的時候,就不會干擾到其他元素的點選事件。
關於這個說法,似乎網上有些爭論,但是我用迄今最新版的 Chrome
Firefox
以及 360瀏覽器
進行測試, 都是上面的結果。
如果你只是想讓元素簡單的漸進顯隱,不用管顯隱元素會不會遮擋什麼點選事件之類的,那麼完全可以不用加 visibility
屬性,加了反而是自找麻煩,但是如果需要考慮到這一點,那麼最好加上。
setTimeOut
如果不使用 visibility
的話還好,但是如果使用了此屬性,那麼上述的解決方案其實還有點小瑕疵,因為 visibility
從 IE10
以及 Android 4.4
才開始支援,如果你需要支援這種版本的瀏覽器,那麼 visibility
就派不上用場了。
哎呦呦,公司網站最低要求都是 IE9
,用不了了誒。
怎麼辦?再回到 display
這個屬性上。
為什麼 display
這個屬性會影響到 transition
動畫呢?
網上有的說法是 因為緩動是基於數值和時間的計算(長度,百分比,角度,顏色也能轉換為數值)(w3.org ),而display
是一個尷尬的屬性,沒辦法轉換。
既然問題是出在了 display
上,那麼我就不用 display
作為過渡的屬性,換成 opocity
,並且讓opocity
與 display
分開執行不就行了嗎?
你如果寫成這種形式:
box1.style.display='block'
box1.style.opacity=1
其實還是沒用的,儘管 display
值的設定在程式碼上看起來好像是在 opacity
前面,但是執行的時候卻是幾乎同時發生的。
我的理解是應該是瀏覽器對程式碼進行了優化,瀏覽器看到你分兩步為同一個元素設定 CSS
屬性,感覺有點浪費,為了更快地完成這兩步,它幫你合併了一下,放在一幀內執行,變成一步到位了,也就是同步執行了這兩句程式碼。
那麼如何明確地讓瀏覽器不要合併執行呢?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上關於此事件的詳細如下:
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: all 3s;
的 樣式,那麼元素可能無論是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事件,此事件是對應 animation
動畫,這裡就不展開了。