1. 程式人生 > >元素顯示與隱藏時的 transition動畫效果原生與框架實現

元素顯示與隱藏時的 transition動畫效果原生與框架實現

近來看到 餓了麼 Apph5站上,在商家詳情頁點餐之後,底部放置了一個點選之後能夠彈出模態框檢視點餐詳情的元素,其中有個背景遮罩層的漸進顯隱的效果。

這裡寫圖片描述

憑著我少許的經驗,第一時間的想法是覺得這個遮罩層應該是使用 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屬性在值 visiblehidden的來回切換中,不會破壞元素的 transition 動畫。

不過 visibilitydisplay 之間控制元素顯隱的最終效果還是有些差別的。

設定了 visibility: hidden; 的元素,視覺上確實是不可見了,但是元素仍然佔據該佔據的位置,仍然會存在於文件流中,影響頁面的佈局,只不過設定了此屬性的元素在視覺上看不到,在頁面的原位置上留下一片空白而已(如果此元素具有寬高並且使用預設定位)。

而設定了 display:none;的元素,其既視覺上不可見,同時也不會佔據空間,也就是說已經從文件流中消失了。

visibility控制元素顯隱同樣是瞬時發生的,不過這種瞬時發生的情況又和 display的那種瞬時發生不太一樣,display是根本不會理會設定的 transition過渡屬性,設定了也和沒設定一樣。

visibility是有可能會理會這個值的,不過只理會 過渡時間 transition-duration這個屬性。

例如,從 visibility: hiddenvisibility: visible;變化時,如果設定了過渡時間為 3s,那麼在事件發生後,元素並不會立即呈現出從hiddenvisible的效果,而是會像下圖那樣,先等待 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: hiddenopacity: 0的元素並不會出現點透。

visibility: hidden的元素就會出現點透,點選事件會穿透 visibility: hidden的元素,被下面的元素接收到,元素在隱藏的時候,就不會干擾到其他元素的點選事件。

關於這個說法,似乎網上有些爭論,但是我用迄今最新版的 Chrome Firefox 以及 360瀏覽器 進行測試, 都是上面的結果。

如果你只是想讓元素簡單的漸進顯隱,不用管顯隱元素會不會遮擋什麼點選事件之類的,那麼完全可以不用加 visibility屬性,加了反而是自找麻煩,但是如果需要考慮到這一點,那麼最好加上。

setTimeOut

如果不使用 visibility的話還好,但是如果使用了此屬性,那麼上述的解決方案其實還有點小瑕疵,因為 visibilityIE10以及 Android 4.4才開始支援,如果你需要支援這種版本的瀏覽器,那麼 visibility 就派不上用場了。

這裡寫圖片描述

哎呦呦,公司網站最低要求都是 IE9,用不了了誒。

怎麼辦?再回到 display 這個屬性上。

為什麼 display 這個屬性會影響到 transition 動畫呢?

網上有的說法是 因為緩動是基於數值和時間的計算(長度,百分比,角度,顏色也能轉換為數值)(w3.org ),而display是一個尷尬的屬性,沒辦法轉換。

既然問題是出在了 display 上,那麼我就不用 display作為過渡的屬性,換成 opocity,並且讓opocitydisplay 分開執行不就行了嗎?

你如果寫成這種形式:

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引數,我在最新版的 Chrome360 瀏覽器上測試,此引數可以不寫,也可以寫成 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;

但是這裡有點不太合理,因為雖然 setTimeOutdelay引數 2000mstransition 時間 2s一樣大,但因為 JS是單執行緒,遵循時間輪詢,所以並不能保證 display屬性的設定剛好是在 opacity過渡完了的同時執行,可能會有更多一點的延遲,這取決於過渡動畫完成之刻,JS主執行緒是否繁忙。

當然,就算是延遲,一般也不會延遲多長時間的,人眼不太可能感覺得到,如果不那麼計較的話其實完全可以無視,但是如果我就吹毛求疵,要想做到更完美,那怎麼辦?

transitionend

transition 動畫結束的時候,對應著一個事件:transitionendMDN上關於此事件的詳細如下:

transitionend 事件會在 CSS transition 結束後觸發. 當 transition完成前移除 transition時,比如移除 csstransition-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'
   });
  }
})

文章最開始說過的 vuereact這兩個框架實現示例動畫的方法,也利用到了這個 API,,監聽動畫過渡的狀態,為元素新增和刪除一系列過渡類名的操作,當然,並不是全部,此事件只能監聽動畫結束的這個時刻,其他時間點是無法監聽的。

  • 以下為 transitionEndreact-addons-css-transition-group原始碼裡面出現的形式:

這裡寫圖片描述

react-addons-css-transition-grouptransitionend做了相容,如果瀏覽器支援此屬性,則使用,如果不支援,就使用 setTimeOut這種形式。

  • 以下為 transitionEndvue原始碼裡面出現的形式:

這裡寫圖片描述

另外,順帶一提的是,除了 transitionend事件,還有一個 animationend事件,此事件是對應 animation動畫,這裡就不展開了。