React NativeにはUIをアニメーションさせる機能があり、こちらの記事で取り上げたアプリでも一部で使用しています。
とはいえ、そのときは作ること優先でかなりベタに書くやり方をしました。今回はアニメーションの実装の仕方について整理したり、OSSのライブラリがどのような実装をしているのか、などを見ていきたいと思います。
ベーシックな書き方
公式ドキュメントのアニメーションのページにある最初のサンプルコードが一番シンプルな事例と言えそうなのでそれを持ってきます。
1class FadeInView extends React.Component {
2 state = {
3 fadeAnim: new Animated.Value(0), // Initial value for opacity: 0
4 }
5
6 componentDidMount() {
7 Animated.timing( // Animate over time
8 this.state.fadeAnim, // The animated value to drive
9 {
10 toValue: 1, // Animate to opacity: 1 (opaque)
11 duration: 10000, // Make it take a while
12 }
13 ).start(); // Starts the animation
14 }
15
16 render() {
17 let { fadeAnim } = this.state;
18
19 return (
20 <Animated.View // Special animatable View
21 style={{
22 ...this.props.style,
23 opacity: fadeAnim, // Bind opacity to animated value
24 }}
25 >
26 {this.props.children}
27 </Animated.View>
28 );
29 }
30}
ポイントは3つかなと思っています。
Animated.Value
アニメーションで変化する値。コンポーネントはこれをstateに持ちます。
styleや色々なpropertyとして使うことができます。
Animated.View
アニメーション可能な<View>で、stateとして持っているAnimated.Valueな値をstyleに持っていて、値の変化に応じてViewが変化していきます。
Animatedには他にも
Animated.ImageAnimated.ScrollViewAnimated.Text
などがあるそうですが、使ったことないです。Text!?って感じです。
Animated.timing
Animated.Valueな値をどのように変化させるか、というアニメーションの肝を司る命令です。この例ではコンポーネントマウント時に値を変化させるため、componentDidMount()内でstartさせています。
と、ここまでは僕の知ってる書き方で、うんまあそうだねという感じ。
ライブラリを覗いてみる
react-native-message-bar
アプリの上にニュッと出てくるアラートみたいなのを手軽に実装できるライブラリで、先のアプリでは通信の失敗のときなんかに使っています。滅多に見ないはず。
メインのコンポーネントを覗いてみます。
react-native-message-bar/MessageBar.js at master · KBLNY/react-native-message-bar
1// 初期設定
2this.animatedValue = new Animated.Value(0);
3
4// 表示するとき
5Animated.timing(this.animatedValue, {
6 toValue: 1,
7 duration: this.state.durationToShow
8}).start(this._showMessageBarAlertComplete());
9
10// 非表示にするとき
11Animated.timing(this.animatedValue, {
12 toValue: 0,
13 duration: this.state.durationToHide
14}).start(this._hideMessageBarAlertComplete());
なるほど、Animated.ValueとAnimated.timingが使われていて、最初の例とあまり変わらないですね。
では、このthis.animatedValueがどのように使われるかを見てみます。
1_apllyAnimationTypeTransformation() {
2// 略
3 switch (animationType) {
4 case 'SlideFromTop':
5 var animationY = this.animatedValue.interpolate({
6 inputRange: [0, 1],
7 outputRange: [-windowHeight, 0]
8 });
9 this.animationTypeTransform = [{ translateY: animationY }];
10 break;
11// 略
ここでのポイントは、this.animationValue.interpolateです。inputRangeの区間をoutputRangeの区間に置き換えます。ここでは0〜1を-windowHeight〜0としています。
ここで代入されているthis.animationTypeTransformは↓な感じでAnimated.Viewのスタイルとして使われています。
1<Animated.View style={{ transform: this.animationTypeTransform, ...
transformYは-windowHeight〜0の間で変化するというわけですね。このようにinterpolateを使うと、Animated.Valueの変化を別の区間にマッピングして使うことができます。
このライブラリは設定によって表示位置変えることができます。そのため、設定に応じてアニメーションに必要なパラメータは異なります。それぞれにAnimated.Valueを用いるのではなく、0〜1に変化する1つのパラメータを設定内容とinterpolateによって使いまわすことでシンプルな実装にできています。
もう1個ぐらい見てみましょう。
react-native-mentions
メンション候補をサジェストしてくれるUIをもったInputです。
こちらは下からニュッと出て、また下にニュッと消える動きですね。
アニメーションの実装部分を抜粋するとこんな感じです。
react-native-mentions/MentionsTextInput.js at master · harshq/react-native-mentions
1// 初期設定
2suggestionRowHeight: new Animated.Value(0),
3
4// 表示
5Animated.timing(this.state.suggestionRowHeight, {
6 toValue: height ? height : this.props.suggestionRowHeight,
7 duration: 100,
8}).start();
9
10// 非表示
11Animated.timing(this.state.suggestionRowHeight, {
12 toValue: 0,
13 duration: 100,
14}).start();
こちらはシンプルに0からheightまで値を変化させてますね。
render()では、
1<Animated.View style={[{ ...this.props.suggestionsPanelStyle }, { height: this.state.suggestionRowHeight }]}>
2 ...
3</Animated.View>
という感じで値をそのまま使っています。シンプル。
おわり
Animated.ValueAnimated.ViewAnimated.timing
の3つに加えて、
interpolate
辺りを把握しておけば基本的なことは大体できそうな感じがしました。
あとアニメーションが絡む場合はコンポーネントに切り出して、アニメーション絡みのコードをコンポーネントに閉じたほうがよさそうです。と、ライブラリのコードを読んで思いました。 初期設定、表示、非表示とそれだけでコード量が増えるんですよね。リファクタリングしたい気持ち。

