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

アニメーション

アニメーションは優れたユーザーエクスペリエンスを作成するために非常に重要です。静止しているオブジェクトは、動き始める際に慣性を克服する必要があります。動いているオブジェクトには勢いがあり、すぐに停止することはめったにありません。アニメーションを使用すると、インターフェースで物理的に現実的な動きを表現できます。

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

Animated API

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

Animatedは、アニメーション化可能な6つのコンポーネントタイプ、ViewTextImageScrollViewFlatList、およびSectionListをエクスポートしますが、Animated.createAnimatedComponent()を使用して独自のコンポーネントを作成することもできます。

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

ここで何が起こっているのかを分析してみましょう。 FadeInViewコンストラクターでは、fadeAnimと呼ばれる新しいAnimated.Valuestateの一部として初期化されます。 Viewのopacityプロパティはこのアニメーション化された値にマッピングされます。バックグラウンドでは、数値が抽出され、opacityの設定に使用されます。

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

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

アニメーションの設定

アニメーションは高度に設定可能です。カスタムおよび定義済みのイージング関数、遅延、期間、減衰係数、バネ定数など、アニメーションのタイプに応じてすべて調整できます。

Animatedはいくつかのアニメーションタイプを提供しますが、最も一般的に使用されるのはAnimated.timing()です。これは、さまざまな定義済みイージング関数のいずれかを使用して、または独自の関数を使用して、時間の経過とともに値をアニメーション化することをサポートします。イージング関数は、通常、アニメーションでオブジェクトの段階的な加速と減速を表現するために使用されます。

デフォルトでは、timingは、完全な速度への段階的な加速を伝え、徐々に減速して停止することで終了するeaseInOut曲線を使用します。 easingパラメータを渡すことで、異なるイージング関数を指定できます。カスタムのduration、またはアニメーションが開始される前のdelayもサポートされています。

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

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

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

アニメーションの合成

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

たとえば、次のアニメーションは停止するまで惰性で動き、その後、並行して回転しながら跳ね返ります。

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

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

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

アニメーション化された値の結合

加算、乗算、除算、または剰余演算を介して2つのアニメーション化された値を結合して、新しいアニメーション化された値を作成できます。

アニメーション化された値が計算のために別のアニメーション化された値を反転する必要がある場合があります。例として、スケールを反転する(2倍 --> 0.5倍)があります。

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

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

補間

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

0〜1の範囲を0〜100の範囲に変換する基本的なマッピングは次のとおりです。

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

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

  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のままになるデッドゾーンを作成するには、次のようにします。

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()は文字列へのマッピングもサポートしており、単位を持つ値だけでなく色もアニメーション化できます。たとえば、回転をアニメーション化する場合、次のようにします。

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

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

動的値の追跡

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

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)にマッピングするために、次のようにします。

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

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

Animated Eventを使用したScrollViewの例

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

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 イベントを使用した PanResponder の例

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

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

  • spring.stopAnimation(callback) はアニメーションを停止し、最終値を使用して callback を呼び出します。これは、ジェスチャートランジションを作成する場合に便利です。
  • spring.addListener(callback) は、アニメーションの実行中に非同期的に callback を呼び出し、最近の値を提供します。これは、状態変更をトリガーする場合に便利です。たとえば、ユーザーがボブルをドラッグして近づけると、新しいオプションにスナップします。これらの大きな状態変更は、60 fps で実行する必要があるパンなどの連続ジェスチャーと比較して、数フレームの遅延の影響を受けにくいためです。

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

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

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

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

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

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

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

<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 アプリを実行し、ネイティブアニメーションの例を読み込むことで、ネイティブドライバーの動作を確認できます。また、ソースコードを見て、これらの例がどのように作成されたかを確認することもできます。

注意点

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

アニメーションの実行中は、VirtualizedListコンポーネントがより多くの行をレンダリングできない場合があります。ユーザーがリストをスクロールしている間に長いアニメーションやループアニメーションを実行する必要がある場合は、アニメーションの設定で`isInteraction: false`を使用してこの問題を防ぐことができます。

留意事項

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

<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 を介して次のフラグを設定する必要があることに注意してください。

UIManager.setLayoutAnimationEnabledExperimental(true);

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

追加の注意事項

requestAnimationFrame

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

setNativeProps

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

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

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