PIYO - Tech & Life -

React Nativeでのアニメーションについて整理しとく

React NativeにはUIをアニメーションさせる機能があり、こちらの記事で取り上げたアプリでも一部で使用しています。

React Nativeで自社サービスのiOS/Androidアプリをリプレイスした話 - PIYO - Tech & Life -
自社サービスRemottyのアプリを作り直すためにReact Nativeを習得してリプレイスしました

とはいえ、そのときは作ること優先でかなりベタに書くやり方をしました。今回はアニメーションの実装の仕方について整理したり、OSSのライブラリがどのような実装をしているのか、などを見ていきたいと思います。

ベーシックな書き方

公式ドキュメントのアニメーションのページにある最初のサンプルコードが一番シンプルな事例と言えそうなのでそれを持ってきます。

Animations · React Native
Animations are very important to create a great user experience. Stationary objects must overcome inertia as they start moving. Objects in motion have momentum and rarely come to a stop immediately. Animations allow you to convey physically believable motion in your interface.
class FadeInView extends React.Component {
  state = {
    fadeAnim: new Animated.Value(0),  // Initial value for opacity: 0
  }

  componentDidMount() {
    Animated.timing(                  // Animate over time
      this.state.fadeAnim,            // The animated value to drive
      {
        toValue: 1,                   // Animate to opacity: 1 (opaque)
        duration: 10000,              // Make it take a while
      }
    ).start();                        // Starts the animation
  }

  render() {
    let { fadeAnim } = this.state;

    return (
      <Animated.View                 // Special animatable View
        style={{
          ...this.props.style,
          opacity: fadeAnim,         // Bind opacity to animated value
        }}
      >
        {this.props.children}
      </Animated.View>
    );
  }
}

ポイントは3つかなと思っています。

Animated.Value

アニメーションで変化する値。コンポーネントはこれをstateに持ちます。

styleや色々なpropertyとして使うことができます。

Animated.View

アニメーション可能な<View>で、stateとして持っているAnimated.Valueな値をstyleに持っていて、値の変化に応じてViewが変化していきます。

Animatedには他にも

  • Animated.Image
  • Animated.ScrollView
  • Animated.Text

などがあるそうですが、使ったことないです。Text!?って感じです。

Animated.timing

Animated.Valueな値をどのように変化させるか、というアニメーションの肝を司る命令です。この例ではコンポーネントマウント時に値を変化させるため、componentDidMount()内でstartさせています。

と、ここまでは僕の知ってる書き方で、うんまあそうだねという感じ。

ライブラリを覗いてみる

react-native-message-bar

アプリの上にニュッと出てくるアラートみたいなのを手軽に実装できるライブラリで、先のアプリでは通信の失敗のときなんかに使っています。滅多に見ないはず。

GitHub - KBLNY/react-native-message-bar: A notification bar alert displayed at the top of the screen for react-native
A notification bar alert displayed at the top of the screen for react-native - KBLNY/react-native-message-bar

メインのコンポーネントを覗いてみます。

react-native-message-bar/MessageBar.js at master · KBLNY/react-native-message-bar

// 初期設定
this.animatedValue = new Animated.Value(0);

// 表示するとき
Animated.timing(this.animatedValue, {
  toValue: 1,
  duration: this.state.durationToShow
}).start(this._showMessageBarAlertComplete());

// 非表示にするとき
Animated.timing(this.animatedValue, {
  toValue: 0,
  duration: this.state.durationToHide
}).start(this._hideMessageBarAlertComplete());

なるほど、Animated.ValueAnimated.timingが使われていて、最初の例とあまり変わらないですね。

では、このthis.animatedValueがどのように使われるかを見てみます。

_apllyAnimationTypeTransformation() {
// 略
  switch (animationType) {
    case 'SlideFromTop':
       var animationY = this.animatedValue.interpolate({
        inputRange: [0, 1],
        outputRange: [-windowHeight, 0]
      });
      this.animationTypeTransform = [{ translateY: animationY }];
      break;
// 略

ここでのポイントは、this.animationValue.interpolateです。inputRangeの区間をoutputRangeの区間に置き換えます。ここでは0〜1-windowHeight〜0としています。

ここで代入されているthis.animationTypeTransformは↓な感じでAnimated.Viewのスタイルとして使われています。

<Animated.View style={{ transform: this.animationTypeTransform, ...

transformY-windowHeight〜0の間で変化するというわけですね。このようにinterpolateを使うと、Animated.Valueの変化を別の区間にマッピングして使うことができます。

このライブラリは設定によって表示位置変えることができます。そのため、設定に応じてアニメーションに必要なパラメータは異なります。それぞれにAnimated.Valueを用いるのではなく、0〜1に変化する1つのパラメータを設定内容とinterpolateによって使いまわすことでシンプルな実装にできています。

もう1個ぐらい見てみましょう。

react-native-mentions

GitHub - harshq/react-native-mentions: Mentions textbox for React Native. Works on both ios and android.
Mentions textbox for React Native. Works on both ios and android. :whale: - harshq/react-native-mentions

メンション候補をサジェストしてくれるUIをもったInputです。

こちらは下からニュッと出て、また下にニュッと消える動きですね。

アニメーションの実装部分を抜粋するとこんな感じです。

react-native-mentions/MentionsTextInput.js at master · harshq/react-native-mentions

// 初期設定
suggestionRowHeight: new Animated.Value(0),

// 表示
Animated.timing(this.state.suggestionRowHeight, {
  toValue: height ? height : this.props.suggestionRowHeight,
  duration: 100,
}).start();

// 非表示
Animated.timing(this.state.suggestionRowHeight, {
  toValue: 0,
  duration: 100,
}).start();

こちらはシンプルに0からheightまで値を変化させてますね。

render()では、

<Animated.View style={[{ ...this.props.suggestionsPanelStyle }, { height: this.state.suggestionRowHeight }]}>
  ...
</Animated.View>

という感じで値をそのまま使っています。シンプル。

おわり

  • Animated.Value
  • Animated.View
  • Animated.timing

の3つに加えて、

  • interpolate

辺りを把握しておけば基本的なことは大体できそうな感じがしました。

あとアニメーションが絡む場合はコンポーネントに切り出して、アニメーション絡みのコードをコンポーネントに閉じたほうがよさそうです。と、ライブラリのコードを読んで思いました。 初期設定、表示、非表示とそれだけでコード量が増えるんですよね。リファクタリングしたい気持ち。