JavaScript
アニメーション
React

ReactMotionで作るリッチなアニメーション(入門用)

More than 1 year has passed since last update.

はじめに

ネイティブアプリの普及やら何やらでWebでもリッチなUIが求められるようになってた昨今、皆様いかがお過ごしでしょうか。

今回はReact.jsでちょっと複雑なアニメーションを作る時に役立つReactMotionの使い方について解説したいと思います。

ReactMotionを使うことで、

  • アニメーションの途中でアニメーションのスピードを徐々に変更する処理を簡単に記述できる
  • 複数の要素が絡むアニメーションを簡単に作ることができる

などを行うことができるようになります。

対象読者

React.jsがちょっと読める

使用したライブラリのバーション

  • Node.js v6.0.0
  • React.js 15.1.0
  • ReactMotion 0.4.3

基本的な使い方

ReactMotionには最も基本的なアニメーションを提供するMotionコンポーネント、複数の兄弟要素の相互作用によってアニメーションを変更できるStaggeredMotionコンポーネント、コンポーネントがDOMツリーに追加されたり削除されたりするタイミングでアニメーションを付与するTransitionMotionコンポーネントがあります。

最初にMotionコンポーネントについて説明しながら、その他のAPIについても説明していきたいと思います。

Motionコンポーネント

まずはデモと具体的なコードから。

サンプルはこちらに置いておきました。
https://github.com/takayuki-ochiai/react-motion-demo

手元で動かしたい人はcloneしたあとnpm install してnpm startしてみてください。
ここではコンポーネントの部分のコードと実際の動きを解説していきたいと思います。

MotionDemo.jsx
import React from 'react';
import { Motion, spring } from 'react-motion';

function MotionDemo() {
  return (
    <Motion defaultStyle={{ x: 0 }} style={{ x: spring(600) }}>
      {interpolatingStyle =>
        <div
          style={{
            fontSize: '40px',
            WebkitTransform: `translate3d(${interpolatingStyle.x}px, 0, 0)`,
          }}
        >
          移動距離 {interpolatingStyle.x} px
        </div>
      }
    </Motion>
  );
}

export default MotionDemo;

実際の動作がこちら。
https://gyazo.com/1193dca8210908ef934d6ce8b76faed6

動きとしてはDOMが左から右に600px移動するだけのシンプルなものです。
アニメーションを作るために、まずdefaulStyleにアニメーションが始まる時の初期値のプロパティをセットします。{ x: 0 }ってやつです。

続いてアニメーションの終了時の値600をReactMotionが提供しているspringという関数に入れてセットします({ x: spring(600) })。このspringの使い方はあとで説明します。

ちなみに、styleに渡す値をspring(600)ではなく600にすると、アニメーションせず初期描画の時点で一気にアニメーション終了時の状態で表示されます。

interpolatingStyleを引数に取る関数の形で書かれている部分が、今回のアニメーションの肝となる部分です。interpolatingStyleはxをプロパティに持つオブジェクトの形式になっていて、xの値はアニメーションの開始から終了までの間に連続的に0から600まで変化していきます。interpolatingStyleの値が変化するたびにDOMの再描画が走ってtranslate3dの値が更新されて、アニメーションとして動く仕組みです。

Motionの中身では、interpolatingStyleの中身の状態の管理はもちろんのこと、アニメーションを変化させる速度についても管理を行っています。今回のデモだとわかりにくいかと思いますが、アニメーションの最初に比べて最後の方が単位時間当たりの移動距離が小さくなっています。
こうした減速処理を1から自分で書くのは大変ですが、ReactMotionを使えばそういった面倒なease-inの処理を1から作らずに済む、というメリットがあります(このレベルなら普通のCSSでも十分に代用は効くとは思いますが・・・)。

この減速処理の内容をもう少し細かく設定したい!という人も多いかと思います。
その場合は、先ほど少し触れたspring関数を利用してください。

spring関数の使い方

spring関数はアニメーション開始から終了まで、アニメーションをどう動かすかを設定するための関数です。基本的な使い方は、先ほどのサンプルコードの通り、

spring(アニメーションの終了時の数値)

という使い方になります。先ほどは初期値が{x: 0}で終了値が{x: spring(600)}だったので、xが0から600まで連続的に変化してinterpolatingStyleに渡されるようになります。
基本的な使い方はそれだけなのですが、アニメーションの動かし方を変えたい場合はオプション値を設定することができます。例えば先ほどのコードを次のように変えてみます。

MotionDemo.jsx
import React from 'react';
import { Motion, spring } from 'react-motion';

function MotionDemo() {
  return (
    <Motion defaultStyle={{ x: 0 }} style={{ x: spring(600, { stiffness: 10, damping: 17 }) }}>
      {interpolatingStyle =>
        <div
          style={{
            fontSize: '40px',
            WebkitTransform: `translate3d(${interpolatingStyle.x}px, 0, 0)`,
          }}
        >
          移動距離 {interpolatingStyle.x} px
        </div>
      }
    </Motion>
  );
}

export default MotionDemo;

実際の動きはこちら。

https://gyazo.com/cb25732194fd752394c8b32c52470dc1

ゆっくり動くようになりました。

渡したパラメーターは下記のようなものでした。
{ stiffness: 10, damping: 17 }

大雑把に説明すると、stiffnessはアニメーションの1コマの間にどれくらい数値が変化するかの初期速度を設定できます。要するに、stiffnessに渡す値を小さくすればアニメーションはゆっくり進むし、値を大きくすればアニメーションは素早く終わります。spring関数はパラメーターを渡さなかった場合はデフォルトでstifnessを170に設定しているので、今回は最初のアニメーションよりゆっくり動く設定になっています。

dampingはアニメーションを変化させる速度のマイナスの加速度みたいなものです。dampingの値が大きければ大きいほど、アニメーション終了時の数値の変化がゆっくりになります。要するにアニメーション終了時の動きをソフトにしたいならdampingの値を大きくしてください。dampingの値を小さくしすぎると、終了地点で止まり切れずに震えるような動き(wobble)になるので注意してください。

stiffnessとdampingの細かいパラメーター調整が必要な時は、下記のページを使えばstiffnessとdampingの組み合わせでどんなアニメーションが起こるのか確認できます。

http://chenglou.github.io/react-motion/demos/demo5-spring-parameters-chooser/

ただ、基本的にはstiffnessとdampingを自分でいじる必要性はそんなないはずですし、自分で設定値をいじらなくてもReactMotionはpresetsというあらかじめ設定されているstiffnessとdampingの組み合わせがいくつか存在します。パラメーターを自分でいじるのはpresetsで自分が実現したい動作ができないか試してみてからでも遅くはないと思います。

StaggeredMotion

複数の要素が絡んだアニメーションを簡単に実現するコンポーネントです。
今回は左から右まで600px移動するコンポーネントを3つ用意し、一個上のコンポーネントが400px移動したタイミングで自分も移動開始するような制御を行ってみました。

StaggeredMotion.jsx
import React from 'react';
import { StaggeredMotion, spring } from 'react-motion';

function StaggeredMotionDemo() {
  return (
    // Motionと違ってpropsの名前がdefaultStyles, stylesと複数形なことに注意すること!
    <StaggeredMotion defaultStyles={[{ x: 0 }, { x: 0 }, { x: 0 }]} styles={
      prevInterpolatedStyles => prevInterpolatedStyles.map((style, i) => {
        if (i === 0) {
          // 一番最初の要素は最初から終端値600まで動かす
          return { x: spring(600, { stiffness: 10, damping: 17 }) };
        } else {
          // 一番目以降の要素は、一つ前の要素のxの値が400まで行ったのを確認した時点で終端値600まで動きだす
          if (prevInterpolatedStyles[i - 1].x > 400) {
            return { x: spring(600, { stiffness: 10, damping: 17 }) };
          } else {
            // 一つ前の要素のxの値が400にいくまでは動かさない
            return { x: 0 };
          }
        }
      })
    }>
      {interpolatingStyles =>
        <div>
          {interpolatingStyles.map((style, _) =>
            <div
              style={{
                fontSize: '40px',
                WebkitTransform: `translate3d(${style.x}px, 0, 0)`,
              }}
            >
              移動距離 {style.x} px
            </div>
          )}
        </div>
      }
    </StaggeredMotion>
  );
}

export default StaggeredMotionDemo;

動きはこんな感じになります。(gif画像で作ったので途中で途切れてしまってますが・・・)
https://gyazo.com/d00df7c0998df978a576ae3d27e57032

最初に3つ分のアニメーションの初期値をdefaultStyles(複数形のprop名であることに注意!)に渡しているのはMotionと同じですが、終了値を決定するstyles(こちらも複数形!)には関数オブジェクトを渡しています。

セットした関数が関数が引数として受け取ってくるprevInterpolatedStylesには1つ前のキーフレーム時のxの値が入っています。これを利用して、今回のコードでは1コマ前の自分の上のコンポーネントの位置が400pxを越えたら自分もアニメーションを動かしはじめる。という制御をしています。

prevInterpolatedStylesには1つ前のフレームの時の設定値が入っていることを利用すると
下記のような面白いこともできます。
本家のサンプルとコードになるのですが、興味がある方は覗いてみてください。

コード
https://github.com/chenglou/react-motion/blob/9cb90eca20ecf56e77feb816d101a4a9110c7d70/demos/demo1-chat-heads/Demo.jsx

デモ
http://chenglou.github.io/react-motion/demos/demo1-chat-heads/

TransitionMotion

コンポーネントがDOMツリーに登録された時と削除された時のアニメーションを決定することができるコンポーネントです。今回はデモとしてDOMが削除が指示されると、幅と高さが0になってから完全に削除される
仕組みを作ってみました。

TransitionMotionDemo.jsx
import React from 'react';
import { TransitionMotion, spring } from 'react-motion';

class TransitionMotionDemo extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      items: [{ key: '1' }, { key: '2' }, { key: '3' }],
    };
    this.onClickDelButton = this.onClickDelButton.bind(this);
  }

  onClickDelButton() {
    const items = this.state.items;
    items.pop();
    this.setState({
      items,
    });
  }

  willLeave() {
    return { width: spring(0), height: spring(0) };
  }

  render() {
    return (
      <div>
        <TransitionMotion
          willLeave={this.willLeave}
          defaultStyles={this.state.items.map(item => ({
            key: item.key,
            style: { width: 0, height: 0 },
          }))}
          styles={this.state.items.map(item => ({
            key: item.key,
            style: { width: spring(1000), height: spring(100) },
          }))}>
          {interpolatedStyles =>
            <div>
              {interpolatedStyles.map(config => {
                return (
                  <div key={config.key} style={{ border: '1px solid', height: config.style.height, width: config.style.width, fontSize: '40px' }}>
                  </div>
                )
              })}
            </div>
          }
        </TransitionMotion>
        <button onClick={this.onClickDelButton}>
          ここを押すと最後の要素の削除が始まってwillLeaveアニメーションスタート
        </button>
      </div>
    );
  }
}

export default TransitionMotionDemo;

https://gyazo.com/22d0b54b508364f308bedffc5a67880e

ここまで読んでくれた方ならなんとなくわかると思いますが、willLeaveのpropに削除されるタイミングで動かすアニメーションのための値を返す関数をセットしておくと、DOMが削除が指示されるタイミングでアニメーションを実行してから、実際にDOMが削除されるようになります。

おわりに

以上、簡単でしたがReactMotionの解説でした。
簡単なアニメーションならreact-addons-css-transition-groupを使ったほうがいいですが、ease-inの時の速度を細かく調整したいとか、複数の要素の相互作用でアニメーションの内容を決定したいとか、ちょっと難しいインタラクションをReactで作りたい時はReactMotionの方が便利だと思います。アニメーションを作る時はぜひ検討してみてください。

最初日本語のドキュメントが全く見つからず苦労したので、そういう苦労が少しでもなくなるといいなと思ってこの記事を書きました。それでは、皆さんよいReactライフをお過ごし下さい。