メインコンテンツへスキップ

アニメーション

優れたユーザーエクスペリエンスを生み出す上で、アニメーションは非常に重要です。静止しているオブジェクトが動き始めるときには、慣性を克服しなければなりません。運動中のオブジェクトには勢いがあり、すぐに停止することはほとんどありません。アニメーションによって、物理的に信憑性のある動きをインターフェースで表現することができます。

React Nativeは2つの補完的なアニメーションシステムを提供しています。Animatedは特定の値に対するきめ細やかでインタラクティブな制御を、LayoutAnimationはアニメーション付きのグローバルなレイアウトトランザクションを実現します。

Animated API

Animated APIは、多種多様な興味深いアニメーションやインタラクションのパターンを、非常に高いパフォーマンスで簡潔に表現できるように設計されています。Animatedは、入力と出力の間の宣言的な関係に焦点を当てており、その間に設定可能な変換を挟み、start/stopメソッドで時間ベースのアニメーション実行を制御します。

Animatedは6つのアニメーション可能なコンポーネント型をエクスポートします: View, Text, Image, ScrollView, FlatList, SectionList ですが、Animated.createAnimatedComponent() を使って独自のコンポーネントを作成することもできます。

例えば、マウントされたときにフェードインするコンテナビューは次のようになります。

ここで何が起こっているかを分解してみましょう。FadeInViewのrenderメソッドでは、fadeAnimという新しいAnimated.ValueuseRefで初期化されます。Viewのopacityプロパティは、このアニメーション化された値にマッピングされます。裏側では、数値が抽出されてopacityの設定に使用されます。

コンポーネントがマウントされると、opacityは0に設定されます。その後、fadeAnimアニメーション値に対してイージングアニメーションが開始され、値が最終値の1にアニメーションするにつれて、各フレームで依存するすべてのマッピング(この場合はopacityのみ)が更新されます。

これはsetStateを呼び出して再レンダリングするよりも高速な、最適化された方法で行われます。設定全体が宣言的であるため、将来的には設定をシリアライズして高優先度のスレッドでアニメーションを実行するなどのさらなる最適化を実装することが可能になります。

アニメーションの設定

アニメーションは高度に設定可能です。カスタムおよび事前定義されたイージング関数、遅延、持続時間、減衰係数、ばね定数などを、アニメーションの種類に応じて調整できます。

Animatedはいくつかのアニメーションタイプを提供しており、最も一般的に使用されるのはAnimated.timing()です。これは、様々な事前定義されたイージング関数のいずれかを使用して、時間をかけて値をアニメーションさせることをサポートしており、独自の関数を使用することもできます。イージング関数は、オブジェクトの段階的な加速や減速を表現するためにアニメーションで一般的に使用されます。

デフォルトでは、timingは、徐々に加速して最高速度に達し、徐々に減速して停止するという動きを表現するeaseInOutカーブを使用します。easingパラメータを渡すことで、異なるイージング関数を指定できます。アニメーションが開始される前のカスタムdurationdelayもサポートされています。

例えば、オブジェクトが最終的な位置に移動する前にわずかに後退する2秒間のアニメーションを作成したい場合

tsx
Animated.timing(this.state.xPosition, {
toValue: 100,
easing: Easing.back(),
duration: 2000,
useNativeDriver: true,
}).start();

組み込みのアニメーションでサポートされているすべての設定パラメータについて詳しく知るには、Animated APIリファレンスのアニメーションの設定セクションをご覧ください。

アニメーションの合成

アニメーションは、連続または並列で組み合わせて再生できます。連続アニメーションは、前のアニメーションが終了した直後に再生することも、指定された遅延の後に開始することもできます。Animated APIはsequence()delay()のようなメソッドを提供しており、それぞれが実行するアニメーションの配列を受け取り、必要に応じて自動的にstart()/stop()を呼び出します。

例えば、以下のアニメーションは惰性で停止した後、並行して回転しながらバネのように跳ね返ります。

tsx
Animated.sequence([
// decay, then spring to start and twirl
Animated.decay(position, {
// coast to a stop
velocity: {x: gestureState.vx, y: gestureState.vy}, // velocity from gesture release
deceleration: 0.997,
useNativeDriver: true,
}),
Animated.parallel([
// after decay, in parallel:
Animated.spring(position, {
toValue: {x: 0, y: 0}, // return to start
useNativeDriver: true,
}),
Animated.timing(twirl, {
// and twirl
toValue: 360,
useNativeDriver: true,
}),
]),
]).start(); // start the sequence group

あるアニメーションが停止または中断されると、グループ内の他のすべてのアニメーションも停止します。Animated.parallelにはstopTogetherオプションがあり、これをfalseに設定することでこの動作を無効にできます。

合成メソッドの完全なリストは、Animated APIリファレンスのアニメーションの合成セクションで確認できます。

アニメーション値の組み合わせ

加算、乗算、除算、または剰余を介して2つのアニメーション値を組み合わせて、新しいアニメーション値を作成できます。

アニメーション値が計算のために他のアニメーション値を反転させる必要がある場合があります。例えば、スケールを反転させる場合(2x --> 0.5x)などです。

tsx
const a = new Animated.Value(1);
const b = Animated.divide(1, a);

Animated.spring(a, {
toValue: 2,
useNativeDriver: true,
}).start();

補間

各プロパティは、最初に補間を介して実行できます。補間は、入力範囲を出力範囲にマッピングします。通常は線形補間を使用しますが、イージング関数もサポートしています。デフォルトでは、指定された範囲を超えてカーブを外挿しますが、出力値をクランプ(制限)させることもできます。

0-1の範囲を0-100の範囲に変換する基本的なマッピングは次のようになります。

tsx
value.interpolate({
inputRange: [0, 1],
outputRange: [0, 100],
});

例えば、Animated.Valueを0から1に進むものとして考え、位置を150pxから0pxに、不透明度を0から1にアニメーションさせたい場合があります。これは、上記の例のstyleを次のように変更することで実現できます。

tsx
  style={{
opacity: this.state.fadeAnim, // Binds directly
transform: [{
translateY: this.state.fadeAnim.interpolate({
inputRange: [0, 1],
outputRange: [150, 0] // 0 : 150, 0.5 : 75, 1 : 0
}),
}],
}}

interpolate()は複数の範囲セグメントもサポートしており、デッドゾーンやその他の便利なトリックを定義するのに便利です。例えば、-300で否定関係になり、-100で0になり、0で1に戻り、100で再び0に戻り、それ以降は0のままのデッドゾーンを続けるには、次のようにします。

tsx
value.interpolate({
inputRange: [-300, -100, 0, 100, 101],
outputRange: [300, 0, 1, 0, 0],
});

これは次のようにマッピングされます。

Input | Output
------|-------
-400| 450
-300| 300
-200| 150
-100| 0
-50| 0.5
0| 1
50| 0.5
100| 0
101| 0
200| 0

interpolate()は文字列へのマッピングもサポートしており、色や単位付きの値をアニメーションさせることができます。例えば、回転をアニメーションさせたい場合は、次のようにできます。

tsx
value.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg'],
});

interpolate()は任意のイージング関数もサポートしており、その多くはEasingモジュールにすでに実装されています。また、interpolate()outputRangeの外挿の挙動を設定可能です。extrapolateextrapolateLeft、またはextrapolateRightオプションを設定することで外挿を設定できます。デフォルト値はextendですが、clampを使用して出力値がoutputRangeを超えないようにすることもできます。

動的な値の追跡

アニメーション値は、アニメーションのtoValueを単なる数値ではなく別のアニメーション値に設定することで、他の値を追跡することもできます。例えば、AndroidのMessengerで使用されているような「チャットヘッド」アニメーションは、別のアニメーション値に固定されたspring()で実装することも、timing()durationを0にして厳密に追跡することもできます。これらは補間と組み合わせることもできます。

tsx
Animated.spring(follower, {toValue: leader}).start();
Animated.timing(opacity, {
toValue: pan.x.interpolate({
inputRange: [0, 300],
outputRange: [1, 0],
}),
useNativeDriver: true,
}).start();

leaderfollowerのアニメーション値はAnimated.ValueXY()を使用して実装されます。ValueXYは、パンやドラッグなどの2Dインタラクションを扱うための便利な方法です。これは2つのAnimated.Valueインスタンスと、それらを呼び出すいくつかのヘルパー関数を含む基本的なラッパーであり、多くの場合、ValueXYValueの代替として使用できます。これにより、上記の例でxとyの両方の値を追跡できます。

ジェスチャーの追跡

パンやスクロールのようなジェスチャーやその他のイベントは、Animated.eventを使用してアニメーション値に直接マッピングできます。これは、複雑なイベントオブジェクトから値を抽出できるように、構造化されたマップ構文で行われます。最初のレベルは複数の引数をマッピングできるように配列になっており、その配列にはネストされたオブジェクトが含まれています。

例えば、水平スクロールジェスチャーを扱う場合、event.nativeEvent.contentOffset.xscrollXAnimated.Value)にマッピングするには、次のようにします。

tsx
 onScroll={Animated.event(
// scrollX = e.nativeEvent.contentOffset.x
[{nativeEvent: {
contentOffset: {
x: scrollX
}
}
}]
)}

次の例では、ScrollViewで使用されているAnimated.eventを使用してスクロール位置インジケーターがアニメーション化される水平スクロールカルーセルを実装しています。

Animated Eventを使用したScrollViewの例

PanResponderを使用する場合、次のコードを使用してgestureState.dxgestureState.dyからxとyの位置を抽出できます。PanResponderハンドラに渡される2番目の引数であるgestureStateにのみ関心があるため、配列の最初の位置にはnullを使用します。

tsx
onPanResponderMove={Animated.event(
[null, // ignore the native event
// extract dx and dy from gestureState
// like 'pan.x = gestureState.dx, pan.y = gestureState.dy'
{dx: pan.x, dy: pan.y}
])}

Animated Eventを使用したPanResponderの例

現在のアニメーション値への応答

アニメーション中に現在の値を読み取る明確な方法がないことに気づくかもしれません。これは、最適化のために値がネイティブランタイムでしかわからない場合があるためです。現在の値に応じてJavaScriptを実行する必要がある場合、2つのアプローチがあります。

  • spring.stopAnimation(callback) はアニメーションを停止し、最終値を引数として callback を呼び出します。これはジェスチャーのトランジションを作成する際に便利です。
  • spring.addListener(callback) は、アニメーションの実行中に非同期で callback を呼び出し、最新の値を提供します。これは、状態の変更をトリガーするのに役立ちます。例えば、ユーザーがボブルを新しいオプションに近づけるとそれにスナップさせるなどです。なぜなら、これらの大きな状態変更は、60fpsで実行する必要があるパンのような連続的なジェスチャーと比較して、数フレームの遅延に対して敏感ではないからです。

Animatedは、通常のJavaScriptイベントループから独立して、高性能な方法でアニメーションを実行できるように、完全にシリアライズ可能に設計されています。これはAPIに影響を与えるため、完全に同期的なシステムと比較して何かを行うのが少しトリッキーに思える場合は、その点を念頭に置いてください。これらの制限の一部を回避する方法としてAnimated.Value.addListenerを確認してください。ただし、将来的にパフォーマンスに影響を与える可能性があるため、慎重に使用してください。

ネイティブドライバーの使用

Animated APIはシリアライズ可能に設計されています。ネイティブドライバーを使用することで、アニメーションを開始する前にアニメーションに関するすべてをネイティブに送信します。これにより、ネイティブコードは各フレームでブリッジを通過することなく、UIスレッドでアニメーションを実行できます。アニメーションが開始されると、JSスレッドがブロックされてもアニメーションに影響はありません。

通常のアニメーションでネイティブドライバーを使用するには、アニメーションを開始する際に設定で useNativeDriver: true を設定します。useNativeDriver プロパティがないアニメーションは、レガシーな理由からデフォルトで false になりますが、警告(およびTypeScriptでは型チェックエラー)を発します。

tsx
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: true, // <-- Set this to true
}).start();

アニメーション値は1つのドライバーとのみ互換性があるため、ある値のアニメーションを開始する際にネイティブドライバーを使用した場合、その値に対するすべてのアニメーションでもネイティブドライバーを使用するようにしてください。

ネイティブドライバーはAnimated.eventでも動作します。これは、スクロール位置に追従するアニメーションに特に便利です。ネイティブドライバーがないと、React Nativeの非同期的な性質のため、アニメーションは常にジェスチャーから1フレーム遅れて実行されます。

tsx
<Animated.ScrollView // <-- Use the Animated ScrollView wrapper
onScroll={Animated.event(
[
{
nativeEvent: {
contentOffset: {y: this.state.animatedValue},
},
},
],
{useNativeDriver: true}, // <-- Set this to true
)}>
{content}
</Animated.ScrollView>

RNTesterアプリを実行し、Native Animated Exampleをロードすることで、ネイティブドライバーの動作を確認できます。また、ソースコードを参照して、これらの例がどのように作成されたかを知ることもできます。

注意点

Animatedでできることのすべてが現在ネイティブドライバーでサポートされているわけではありません。主な制限は、非レイアウトプロパティのみをアニメーション化できることです。transformopacityのようなものは動作しますが、Flexboxやpositionプロパティは動作しません。Animated.eventを使用する場合、直接イベントでのみ機能し、バブリングイベントでは機能しません。これは、PanResponderでは機能しないが、ScrollView#onScrollのようなものでは機能することを意味します。

アニメーションが実行中であると、VirtualizedListコンポーネントがそれ以上の行をレンダリングするのを妨げる可能性があります。ユーザーがリストをスクロールしている間に長いアニメーションやループアニメーションを実行する必要がある場合は、アニメーションの設定でisInteraction: falseを使用してこの問題を回避できます。

留意事項

rotateYrotateXなどのtransformスタイルを使用する場合は、transformスタイルperspectiveが設定されていることを確認してください。現時点では、これがないと一部のアニメーションがAndroidでレンダリングされない場合があります。以下に例を示します。

tsx
<Animated.View
style={{
transform: [
{scale: this.state.scale},
{rotateY: this.state.rotateY},
{perspective: 1000}, // without this line this Animation will not render on Android while working fine on iOS
],
}}
/>

追加の例

RNTesterアプリには、Animatedを使用した様々な例があります。

LayoutAnimation API

LayoutAnimationを使用すると、次のレンダー/レイアウトサイクルですべてのビューに使用されるcreateおよびupdateアニメーションをグローバルに設定できます。これは、特定のプロパティを直接アニメーション化するために測定や計算をする手間をかけずにFlexboxのレイアウト更新を行うのに便利です。特に、親のサイズも大きくし、下の行を押し下げるような「もっと見る」の展開など、レイアウトの変更が祖先に影響を与える可能性がある場合に役立ちます。このような場合、通常はすべてのコンポーネントを同期してアニメーション化するために、コンポーネント間で明示的な調整が必要になります。

LayoutAnimationは非常に強力で便利ですが、Animatedや他のアニメーションライブラリよりも制御できる範囲がはるかに少ないことに注意してください。したがって、LayoutAnimationでやりたいことが実現できない場合は、別のアプローチを使用する必要があるかもしれません。

これをAndroidで動作させるには、UIManagerを介して次のフラグを設定する必要があることに注意してください。

tsx
UIManager.setLayoutAnimationEnabledExperimental(true);

この例ではプリセット値を使用していますが、必要に応じてアニメーションをカスタマイズできます。詳細については、LayoutAnimation.jsを参照してください。

補足

requestAnimationFrame

requestAnimationFrameは、ブラウザから来たポリフィルで、ご存知かもしれません。これは唯一の引数として関数を受け取り、次の再描画の前にその関数を呼び出します。これは、すべてのJavaScriptベースのアニメーションAPIの基礎となる、アニメーションにとって不可欠な構成要素です。一般的に、これを自分で呼び出す必要はありません。アニメーションAPIがフレームの更新を管理してくれます。

setNativeProps

直接操作のセクションで述べたように、setNativePropsを使用すると、setStateしてコンポーネント階層を再レンダリングすることなく、ネイティブに裏打ちされたコンポーネント(複合コンポーネントとは異なり、実際にネイティブビューによって裏打ちされているコンポーネント)のプロパティを直接変更できます。

Reboundの例でこれを使用してスケールを更新することができます。これは、更新しているコンポーネントが深くネストされており、shouldComponentUpdateで最適化されていない場合に役立つかもしれません。

アニメーションでフレーム落ちが発生する場合(毎秒60フレーム未満で実行される場合)は、setNativePropsまたはshouldComponentUpdateを使用して最適化することを検討してください。あるいは、useNativeDriverオプションを使用して、JavaScriptスレッドではなくUIスレッドでアニメーションを実行することもできます。また、InteractionManagerを使用して、計算量の多い作業をアニメーションが完了するまで遅延させることもできます。アプリ内開発者メニューの「FPS Monitor」ツールを使用してフレームレートを監視できます。