アニメーション
優れたユーザーエクスペリエンスを生み出す上で、アニメーションは非常に重要です。静止しているオブジェクトが動き始めるときには、慣性を克服しなければなりません。運動中のオブジェクトには勢いがあり、すぐに停止することはほとんどありません。アニメーションによって、物理的に信憑性のある動きをインターフェースで表現することができます。
React Nativeは2つの補完的なアニメーションシステムを提供しています。Animated
は特定の値に対するきめ細やかでインタラクティブな制御を、LayoutAnimation
はアニメーション付きのグローバルなレイアウトトランザクションを実現します。
Animated
API
Animated
APIは、多種多様な興味深いアニメーションやインタラクションのパターンを、非常に高いパフォーマンスで簡潔に表現できるように設計されています。Animated
は、入力と出力の間の宣言的な関係に焦点を当てており、その間に設定可能な変換を挟み、start
/stop
メソッドで時間ベースのアニメーション実行を制御します。
Animated
は6つのアニメーション可能なコンポーネント型をエクスポートします: View
, Text
, Image
, ScrollView
, FlatList
, SectionList
ですが、Animated.createAnimatedComponent()
を使って独自のコンポーネントを作成することもできます。
例えば、マウントされたときにフェードインするコンテナビューは次のようになります。
- TypeScript
- JavaScript
ここで何が起こっているかを分解してみましょう。FadeInView
のrenderメソッドでは、fadeAnim
という新しいAnimated.Value
がuseRef
で初期化されます。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
あるアニメーションが停止または中断されると、グループ内の他のすべてのアニメーションも停止します。Animated.parallel
にはstopTogether
オプションがあり、これをfalse
に設定することでこの動作を無効にできます。
合成メソッドの完全なリストは、Animated
APIリファレンスのアニメーションの合成セクションで確認できます。
アニメーション値の組み合わせ
加算、乗算、除算、または剰余を介して2つのアニメーション値を組み合わせて、新しいアニメーション値を作成できます。
アニメーション値が計算のために他のアニメーション値を反転させる必要がある場合があります。例えば、スケールを反転させる場合(2x --> 0.5x)などです。
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に、不透明度を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
の外挿の挙動を設定可能です。extrapolate
、extrapolateLeft
、またはextrapolateRight
オプションを設定することで外挿を設定できます。デフォルト値はextend
ですが、clamp
を使用して出力値がoutputRange
を超えないようにすることもできます。
動的な値の追跡
アニメーション値は、アニメーションのtoValue
を単なる数値ではなく別のアニメーション値に設定することで、他の値を追跡することもできます。例えば、AndroidのMessengerで使用されているような「チャットヘッド」アニメーションは、別のアニメーション値に固定されたspring()
で実装することも、timing()
とduration
を0にして厳密に追跡することもできます。これらは補間と組み合わせることもできます。
Animated.spring(follower, {toValue: leader}).start();
Animated.timing(opacity, {
toValue: pan.x.interpolate({
inputRange: [0, 300],
outputRange: [1, 0],
}),
useNativeDriver: true,
}).start();
leader
とfollower
のアニメーション値はAnimated.ValueXY()
を使用して実装されます。ValueXY
は、パンやドラッグなどの2Dインタラクションを扱うための便利な方法です。これは2つのAnimated.Value
インスタンスと、それらを呼び出すいくつかのヘルパー関数を含む基本的なラッパーであり、多くの場合、ValueXY
はValue
の代替として使用できます。これにより、上記の例でxとyの両方の値を追跡できます。
ジェスチャーの追跡
パンやスクロールのようなジェスチャーやその他のイベントは、Animated.event
を使用してアニメーション値に直接マッピングできます。これは、複雑なイベントオブジェクトから値を抽出できるように、構造化されたマップ構文で行われます。最初のレベルは複数の引数をマッピングできるように配列になっており、その配列にはネストされたオブジェクトが含まれています。
例えば、水平スクロールジェスチャーを扱う場合、event.nativeEvent.contentOffset.x
をscrollX
(Animated.Value
)にマッピングするには、次のようにします。
onScroll={Animated.event(
// scrollX = e.nativeEvent.contentOffset.x
[{nativeEvent: {
contentOffset: {
x: scrollX
}
}
}]
)}
次の例では、ScrollView
で使用されているAnimated.event
を使用してスクロール位置インジケーターがアニメーション化される水平スクロールカルーセルを実装しています。
Animated Eventを使用したScrollViewの例
PanResponder
を使用する場合、次のコードを使用してgestureState.dx
とgestureState.dy
からxとyの位置を抽出できます。PanResponder
ハンドラに渡される2番目の引数であるgestureState
にのみ関心があるため、配列の最初の位置にはnull
を使用します。
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では型チェックエラー)を発します。
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アプリを実行し、Native Animated Exampleをロードすることで、ネイティブドライバーの動作を確認できます。また、ソースコードを参照して、これらの例がどのように作成されたかを知ることもできます。
注意点
Animated
でできることのすべてが現在ネイティブドライバーでサポートされているわけではありません。主な制限は、非レイアウトプロパティのみをアニメーション化できることです。transform
やopacity
のようなものは動作しますが、Flexboxやpositionプロパティは動作しません。Animated.event
を使用する場合、直接イベントでのみ機能し、バブリングイベントでは機能しません。これは、PanResponder
では機能しないが、ScrollView#onScroll
のようなものでは機能することを意味します。
アニメーションが実行中であると、VirtualizedList
コンポーネントがそれ以上の行をレンダリングするのを妨げる可能性があります。ユーザーがリストをスクロールしている間に長いアニメーションやループアニメーションを実行する必要がある場合は、アニメーションの設定でisInteraction: false
を使用してこの問題を回避できます。
留意事項
rotateY
、rotateX
などのtransformスタイルを使用する場合は、transformスタイル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
で最適化されていない場合に役立つかもしれません。
アニメーションでフレーム落ちが発生する場合(毎秒60フレーム未満で実行される場合)は、setNativeProps
またはshouldComponentUpdate
を使用して最適化することを検討してください。あるいは、useNativeDriverオプションを使用して、JavaScriptスレッドではなくUIスレッドでアニメーションを実行することもできます。また、InteractionManagerを使用して、計算量の多い作業をアニメーションが完了するまで遅延させることもできます。アプリ内開発者メニューの「FPS Monitor」ツールを使用してフレームレートを監視できます。