React Nativeでアニメーションを実装する場合は、Animatedコンポーネントを使う。

Animatedには多岐にわたる機能が含まれるが、今回は最も基本的なフェードインアニメーションの実装方法を紹介する。

Animatedコンポーネントのインポート

まずはAnimatedをインポートする必要がある。

import Animated from 'react-native';

Animated.Viewの使い方

今回は特定の要素(View)をフェードインさせるアニメーションを実装する。

例として、以前の記事で作ったニュースアプリを使って、記事が読み込まれたら記事リストを透明状態から徐々にフェードインさせてみる。

アニメーション実装対象の要素をラップする

まずはアニメーションをつけたい要素。今回の場合は記事リストにあたるFlatViewをAnimated.Viewで囲む。

また、アニメーションのためのスタイルとしてopacity属性をAnimated.Viewに設定する。
opacityはCSSにも登場する属性で、0は透明状態を、1は不透明(表示状態)を表している。

<Animated.View style={{ opacity: opacity}}>
<FlatList
  data={threads}
  renderItem={({item}) => {
    return(
      <View style={{
        flex: 1,
        flexDirection: 'row',
        width: '100%'
      }}>
        <Image
          style={{
            width: 50,
            height: 50
          }}
          source={item.data.thumbnail}
        />
          <View style={{
            flex: 1,
            flexDirection: 'column'
          }}>
            <Text>{item.data.title}</Text>
            <Text style={{color: '#ababab', fontSize: 10}}>{item.data.domain}</Text>
          </View>
      </View>
    )
  }}
/>
</Animated.View>

opacityをstate変数で管理する

opacityの値はコンストラクタ内でstate変数として定義して管理する。

constructor() {
  super();
  this.state = {
    isLoading: true,
    threads: [],
    opacity: new Animated.Value(0),
  }
}

Animated.Valueの引数にはopacityの初期値を指定する。
今回は透明状態からフェードインさせるため0にしておく。

opacity値を変化させるメソッドを定義する

次にstate変数として管理するopacityの値を、アニメーションによって変化させるメソッドを定義する。

Animated.timingは第一引数に値を変化させる変数を、第二引数にアニメーション動作を設定するための属性値を指定する。

animate() {
  Animated.timing(this.state.opacity, {
    toValue: 1,
    duration: 1000,
  }).start();
}

toValueは変数の最終値を、durationは最終値に至るまでの時間をms(ミリ秒)単位で指定する。

今回定義したanimateメソッドの内容だと、初期値0のopacityを1000ms(1秒)かけて1(表示状態)にするという事になる。

これまで作ってきたニュースアプリへの実装

それではニュースアプリへ実装してみよう。

記事の読み込みが終了したタイミングでanimateメソッドを実行し、記事リストをフェードインさせてみる。

import React, { Component } from 'react';
import {
  Text, View, FlatList, Image, Dimensions, ActivityIndicator, Animated
} from 'react-native';

export default class App extends Component {
  constructor() {
    super();
    this.state = {
      isLoading: true,
      threads: [],
      opacity: new Animated.Value(0),
    }
  }

  animate() {
    Animated.timing(this.state.opacity, {
      toValue: 1,
      duration: 1000,
    }).start();
  }

  componentDidMount() {
    fetch("https://www.reddit.com/r/newsokur/hot.json")
      .then((response) => response.json())
      .then((responseJson) => {
        let threads = responseJson.data.children;
        threads = threads.map(i => {
          i.key = i.data.url;
          return i;
        });

        this.setState({threads: threads, isLoading: false});
      })
      .catch((error) => {
        console.error(error);
      })
  }

  render() {
    const { threads, isLoading, opacity } = this.state;
    const { width } = Dimensions.get('window');

    if(!isLoading)
      this.animate();

    return(
      <View style={{
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
      }}>
        {isLoading ? <ActivityIndicator /> :
          <Animated.View style={{ opacity: opacity}}>
          <FlatList
            data={threads}
            renderItem={({item}) => {
              return(
                <View style={{
                  flex: 1,
                  flexDirection: 'row',
                  width: '100%'
                }}>
                  <Image
                    style={{
                      width: 50,
                      height: 50
                    }}
                    source={item.data.thumbnail}
                  />
                    <View style={{
                      flex: 1,
                      flexDirection: 'column'
                    }}>
                      <Text>{item.data.title}</Text>
                      <Text style={{color: '#ababab', fontSize: 10}}>{item.data.domain}</Text>
                    </View>
                </View>
              )
            }}
          />
          </Animated.View>
        }
      </View>
    )
  }
}

render内、41行目でstate値をopacityへ代入し、44・45行目でローディングが終了したらanimateメソッドを実行するようにしている。

expo startでアプリを実行すると、ローディングマークが表示され、記事の読み込みが完了するとフワッと記事リストが表示されるはずだ。