Vue.js でたのしいトランジション 通常編

雰囲気で使うのを卒業するべく少し詳しく調べてみました。

凝った自作のアニメーション遷移を作る場合デザインとJavaScript両方の知識が必要になる事が多いですが Vue のトランジション機能を使うと比較的楽に作ることが出来ます。

トランジションで付加されるクラスが色々あって、複雑なアニメーションCSSを作りたい時どのクラスにどういうスタイルつければ良いんだっけ?と迷ってしまう事があると思うんですが、クラスの付くタイミングを覚えておくととても楽になります。

クラス名とプレフィックス

トランジションは切り替わる要素を <transition></transition> タグで囲むだけで使用できます。display:none や DOM からの要素の削除は Vue がやってくれるので基本的にCSSでアニメーションだけ作ればOK。

表示される時(enter)・消える時(leave)それぞれのクラス名を使用できるよう��なります。クラス名はタグのオプションで名前を設定しない場合デフォルトで v- というプレフィックスが付きます。scoped CSS を使う場合は被りを気にしなくてもいいのでデフォルトで良いかも。

.v-enter-active, .v-leave-active {
  transition: opacity 1s;
}
.v-enter, .v-leave-to {
  opacity: 0;
}

名前をつけた場合はその名前がプレフィックスになります。

<transition name="demo"></transition>

この場合は v- の部分が demo- になります。

.demo-enter-active, .demo-leave-active {
  transition: opacity 1s;
}
.demo-enter, .demo-leave-to {
  opacity: 0;
}

クラスの種類とトランジション中のクラスの状態

トランジションの内部では requestAnimationFramesetTimeout を使って非同期なフレームレート処理がされてるみたいです。

公式の画像と大体一緒ですが少し詳しく画像とアニメーションを作りました。以下の例は7フレームを使う俊足なアニメーションのクラスの状態です。

トランジションの中間の状態はイージングやディレイによって変わるのでクラスと開始&終了の状態だけ参考にしてください!

enter 非表示から表示

  • トランジション開始 v-enter & v-enter-active 付いた状態で要素が追加される
  • 次のフレームで v-enter が削除されて v-enter-to が追加される
  • トランジション終了 v-enter-activev-enter-to が削除される

leave 表示から非表示

  • トランジション開始 v-leavev-leave-active が追加される
  • 次のフレームで v-leave が削除されて v-leave-to が追加される
  • トランジション終了 v-leave-active & v-leave-to が削除され要素が削除される

アニメーションで見る

これは目視しやすいように1fpsにしてます。

enter の場合だと1フレーム目は DOM への追加と v-enter/v-enter-active クラスの付加、v-enter が削除されて v-enter-to になった2フレーム目から opacity:0 から opacity:1 へのCSSトランジションが始まります。

トランジションを作る

アニメーションを作る時の基本は v-enter/v-leave から v-enter-to/v-leave-to への CSSトランジションです。v-enter-active/v-leave-activetransition プロパティの指定と JavaScript で何か操作する時に使うぐらいで、このクラスに装飾的なスタイルを設定する必要はほぼありません。

おうちゃくして対象の要素(.item)に直接CSSトランジションを all で設定してますが .v-enter-active, .v-leave-active に設定したほうがホバー用など他のアニメーションに影響しません。

おうちゃくする場合

.item {
  transition: all 1s;
}

ちゃんと書く場合

.v-enter-active, .v-leave-active {
  transition: opacity 1s;
}

基本的なトランジション

demo1 はフェードイン&フェードアウトの簡単なトランジションです。

/* トランジション中有効にする CSS transition プロパティ */
.demo1-enter-active, .demo1-leave-active {
  transition: opacity 1s;
}
/* 表示される時は0から1へ*/
.demo1-enter {
  opacity: 0;
}
.demo1-enter-to {
  opacity: 1;
}
/* 消える時は1から0へ*/
.demo1-leave {
  opacity: 1;
}
.demo1-leave-to {
  opacity: 0;
}

元々何も付いていなければ opacity:1 の初期状態なので demo1-enter-todemo1-leave は省略できます。また、demo1-enterdemo1-leave-to はスタイルが同じでまとめられるのでこんな感じに書けますね(๑'ᴗ'๑) こうすると公式のサンプルと同じになりました。

.demo1-enter-active, .demo1-leave-active {
  transition: opacity 1s;
}
.demo1-enter, .demo1-leave-to {
  opacity: 0;
}

表示/非表示で状態が違うトランジション

demo2 は 開始と終了で動きを変えたトランジションです。

/* 遷移後に文字色をアニメーションで戻すために要素自体にも設定 */
.item {
  transition: color 2s;
}
/* 表示するときはゆっくり@文字色だけアニメーションさせない */
/* 遷移中は文字色を透明にしとく */
.demo2-enter-active {
  color: transparent;
  transition: all 1s, color 0s;
}
/* 消えるときは素早く */
.demo2-leave-active {
  transition: all .5s;
}
/* 表示するときは透明度0の左側から */
.demo2-enter {
  opacity: 0;
  transform: translateX(-50px);
}
/* 消えるときは透明度0の左側へ */
.demo2-leave-to {
  opacity: 0;
  transform: translateX(50px);
}

表示される時は左からゆっくりスライドして消える時は素早く右にスライドするようになります。

遷移が終わったあとのアニメーションは他に影響しなければこれでもいいですが、ホバー時のアニメーションでは速さを 1s にしたいとか細かく設定したい場合は要素自体に付けるよりもタイマーを設定して Vue と同じようにクラスを付けるほうが影響しないかなと思います。

スライドで入れ替わるトランジション

demo3 は複数のコンポーネントを入れ替えるトランジションです。

.demo3-enter-active, .demo3-leave-active {
  transition: all 1s;
}
.demo3-enter {
  opacity: 0;
  transform: translateX(-100px) scale(1.2);
}
.demo3-leave-to {
  opacity: 0;
  transform: translateX(100px) scale(0.8);
}
.demo3-leave-active {
  position: absolute;
}

消す時に position:absolute にして重ねておいて transform で見える位置をずらしています。こんなのも簡単にできる!ルータービューとかに使うとチョットいい感じ。

同じ要素をトランジションする場合はユニークなキーを設定します。

<transition name="demo3">
  <div class="item" v-if="current==0" key="1">item1</div>
  <div class="item" v-if="current==1" key="2">item2</div>
  <div class="item" v-if="current==2" key="3">item3</div>
</transition>

結果が複数の要素になるトランジションは transition-group を使います。

トランジションで高さが変わる時にアニメーションする

これは状態のトランジションなので Vue の transition タグとは別に設定します。非同期コンテンツが入ると結構めんどくさいですが、色々方法はあると思うんですけどサンプルはラッパー要素とウォッチャを使った例です。

ウォッチャを使ってアニメーションする場合

<div id="box" :style="{height: height}">
  <div id="box-body">
    <transition name="demo4">
      <div class="item" key="1" v-if="current">item1</div>
      <div class="item" key="2" v-if="!current" style="height:50px;">item2</div>
    </transition>
  </div>
</div>
.demo4-enter-active, .demo4-leave-active {
  transition: all 1s;
}
.demo4-enter, .demo4-leave-to {
  opacity: 0;
}
.demo4-leave-active {
  position: absolute;
}
#box {
  transition: height 1s;
  overflow: hidden;
  border: 2px solid #ccc;
  padding: 10px;
}
new Vue({
  el: '#app',
  data: {
    current: true,
    height: '',
    timer: null
  },
  watch: {
    current() {
      if (this.timer) { clearTimeout(this.timer) }
      var elWrap = document.getElementById('box-body')
      // 変化があったら今の高さを取得して設定
      var oldHeight = elWrap.getBoundingClientRect().height
      this.height = oldHeight + 'px'
      this.$nextTick(() => {
        // DOMが更新されたあとに再取得して設定
        var height = elWrap.getBoundingClientRect().height
        this.height = height + 'px'
        // 後から高さが変化してもいいようにアニメーションが終わったら高さ指定を消す
        this.timer = setTimeout(() => { this.height = '' }, 1000)
      })
    }
  }
})

なんとか CSS だけでやりたい場合

一応ラッパーの方にトランジションを施す事で高さをアニメーションする方法もあります。max-height も使うとある程度の可変サイズも JavaScript を書かずに CSS だけで出来るけどデュレーションやイージングが指定通りにならないのと CSS もややこしくなるのであまりオススメできない。

<transition name="demo">
  <div class="wrapper" v-if="current">
    <div class="item">item</div>
  </div>
</transition>
.wrapper {
  transition: height 0.5s;
  height: 100px;
}
.item {
  transition: opacity 1s;
}
.demo1-enter, .demo1-leave-to {
  height: 0px;
  overflow: hidden;
}
.demo1-enter .item, .demo1-leave-to .item {
  opacity: 0;
}

トランジションフック

フックを使うとトランジションに処理を挟んだりスクリプトでアニメーション処理を作成できます。

クラスとフックのタイミングはこのソースコードを見るとよく分かります。

src/platformsmodules/transition.js

CSS用クラスと一緒に使うこともできるけど :css="false" を設定すると Vue 側でクラスとフレームの処理がされないので完全に JavaScript で制御したい場合は都合がいい。ちなみにその場合は要素の追加/削除以外の変化は無く、各フックも同期なのでクラスを付けても setTimeout(0) などして自分でフレーム処理しないと CSS トランジションも開始されません。

before-enter / enter / after-enter / enter-cancelled

  • before-enter DOM に要素が追加される前、重複でなければクラスが付く前
  • enter .v-enter が付いて DOM に要素が追加された後、CSS:false なら before-enter の直後
  • after-enter enterdone() したら
  • enter-cancelled 重複して途中キャンセルした時

before-leave / leave / after-leave / leave-cancelled

  • before-leave 重複でなければクラスが付く前
  • leave .v-leave が付いた後、CSS:false なら before-leave の直後
  • after-leave leavedone() した後で DOM から要素が削除された後
  • leave-cancelled 重複して途中キャンセルした時 (v-showを使った時のみ。他はその代わりかどうかはワカラナイですがafter-leaveが発生)

確認してみた所だいたいこんな感じかなと思います。after-enter/after-leaveCSS:false に設定して enter/leave のフックを使用している場合 done() しない限り発生しない。enter/leaveを使っていなければ即座に発生。

最初に表示するときにも再生したい場合

<transition></transition> タグに appear を付けるだけでOK。最初に表示するときもトランジションされるようになる。

<transition appear>
  <div v-if="fade">...</div>
</transition>

何も設定しない場合 v-enter/v-leave のクラスやフックが使われますが、初期表示を区別して何かしたい場合は名前をカスタマイズ出来ます。

表示したまま状態を変えたい場合

一つの要素に enter/leave 両方の状態を通して変化させるという事は出来ない。(要素がコピーされてしまう)単純に色や位置などの状態を変えたい場合は CSS の transition を設定するだけで可能なパターンが多いです。それ以外は公式マニュアルにある「状態のトランジション」の方法を使ったり、ウォッチャやカスタムディレクティブを使って自分で要素のクラスやスタイルを変更すると良いです。

まとめ

あんまりゴチャゴチャしてるのもあれですけどちょっと動きを付けると楽しいですね!

ところで Vue のトランジションタグは SVG の中の要素にも使うことが出来るんですよ。便利(っ'ロ'c)🍣

次回たのしいトランジションリスト編 でぬるぬる動くリストの作り方も書いてるのでぜひ見てください。