先日、Animatedを使ったアニメーションの実装方法について以下の記事で紹介した。

今回はAnimatedを使った無限ループアニメーションの実装方法を紹介していく。

start()メソッドのコールバック関数

まずは前回書いたanimatedメソッドの内容を確認しよう。

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

start()メソッドでアニメーションを実行するのだが、ここにコールバック関数を指定することで無限ループを実現することが可能になる。

このコールバック関数は、指定したアニメーションが終了した際に呼び出されるので、関数内で更にアニメーションを実行することで無限ループを実装できる。

前回までのコードと今回の目標物

前回記事で作成したサンプルアプリのコードを以下に記載しておく。

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>
    )
  }
}

APIで記事を読み込み、リスト表示するだけのアプリだ。

記事の読み込み中は、ActivityIndicatorを使ってローディングマークを表示させているのだが、これを別の画像(今回は猫のイラスト画像)に代替し、無限ループを使ってグルグル回り続けるようにコードを書き換えていく。

無限ループ用のコンポーネントを追加する

まずはコード最下部に以下のコンポーネントを追加する。

class Spining extends Component {
  constructor() {
    super();
    this.state = {
      degree: new Animated.Value(0)
    }
  }

  render() {
    const { degree } = this.state;
    const _degree = degree.interpolate({
      inputRange: [0, 1],
      outputRange: ['0deg', '360deg'],
    })

    return (
      <View>
        <Animated.Image
          source={require('./assets/cat.jpg')}
          style={{
            transform:[{rotate: _degree}],
            width: 50,
            height: 50,
          }}
        />
      </View>
    )
  }
}

コード内にinterpolateという関数があるが、これはoutputRangeに指定した初期値・終了値を、inputRangeで指定する初期値・終了値に置き換えることのできる便利な関数だ。
アニメーションのように特定の値を滑らかに動かす場合に重宝する。

次にメインとなるanimated()メソッド、更にコンポーネントがビューにマウントされた際に自動的にアニメーションが実行されるよう、ライフサイクル関数を定義する。

最終的に出来上がったコンポーネントがこちら。

class Spining extends Component {
  constructor() {
    super();
    this.state = {
      degree: new Animated.Value(0)
    }
  }

  componentDidMount() {
    this.animated()
  }
  componentWillUnmount() {
    this.animated = () => {
      return false
    }
  }

  animated() {
    Animated.timing(
      this.state.degree,
      {
        toValue: 1,
        duration: 4000,
      }
    ).start(() => {
      this.setState({degree: new Animated.Value(0)})
      this.animated()
    });
  }

  render() {
    const { degree } = this.state;
    const _degree = degree.interpolate({
      inputRange: [0, 1],
      outputRange: ['0deg', '360deg'],
    })

    return (
      <View>
        <Animated.Image
          source={require('./assets/cat.jpg')}
          style={{
            transform:[{rotate: _degree}],
            width: 50,
            height: 50,
          }}
        />
      </View>
    )
  }
}

メインコンポーネントから呼び出す

最後にメインのAppコンポーネントから、追加したSpiningコンポーネントを呼び出す。

コード内、ローディング中に表示するActivityIndicatorをSpiningに置き換える。

{isLoading ? <ActivityIndicator /> :
↓ 以下に変更
{isLoading ? <Spining /> :

ただ、これだけでは記事の読み込みが終わると記事リストが表示され、無限ループのアニメーションが確認できないので、AppコンポーネントのcomponentDidMount()メソッド内、state値の変更をコメントアウトし無効化する。

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);
    })
}

ここまでの変更を適用したコード全文がこちら。

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 ? <Spining /> :
          <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>
    )
  }
}

class Spining extends Component {
  constructor() {
    super();
    this.state = {
      degree: new Animated.Value(0)
    }
  }

  componentDidMount() {
    this.animated()
  }
  componentWillUnmount() {
    this.animated = () => {
      return false
    }
  }

  animated() {
    Animated.timing(
      this.state.degree,
      {
        toValue: 1,
        duration: 4000,
      }
    ).start(() => {
      this.setState({degree: new Animated.Value(0)})
      this.animated()
    });
  }

  render() {
    const { degree } = this.state;
    const _degree = degree.interpolate({
      inputRange: [0, 1],
      outputRange: ['0deg', '360deg'],
    })

    return (
      <View>
        <Animated.Image
          source={require('./assets/cat.jpg')}
          style={{
            transform:[{rotate: _degree}],
            width: 50,
            height: 50,
          }}
        />
      </View>
    )
  }
}

expo startコマンドを実行するとサンプルアプリが立ち上がる。

猫のイラストがグルグル回るアニメーションが表示されればOK。