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

アニメーションにネイティブドライバーを使用する

·7 分で読める
Janic Duplessis
App & Flowのソフトウェアエンジニア

昨年、私たちはAnimatedライブラリを使用するアニメーションのパフォーマンス向上に取り組んできました。アニメーションは美しいユーザーエクスペリエンスを作成するために非常に重要ですが、正しく行うのは難しい場合もあります。開発者がコードのラグを心配することなく、パフォーマンスの高いアニメーションを簡単に作成できるようにしたいと考えています。

これは何ですか?

Animated APIは、非常に重要な制約、つまりシリアライズ可能であることを念頭に置いて設計されました。つまり、アニメーションが始まる前に、アニメーションに関するすべてをネイティブに送信できます。これにより、ネイティブコードは、フレームごとにブリッジを通過することなく、UIスレッドでアニメーションを実行できます。アニメーションが開始されると、JSスレッドがブロックされてもアニメーションはスムーズに実行されるため、非常に便利です。実際には、ユーザーコードはJSスレッドで実行され、ReactのレンダリングもJSを長時間ロックする可能性があるため、これは頻繁に発生する可能性があります。

歴史を少し...

このプロジェクトは約1年前、ExpoがAndroidでli.stアプリを構築したときに開始されました。Krzysztof Magiera氏は、Androidでの初期実装を構築するために契約されました。最終的にはうまく機能し、li.stはAnimatedを使用してネイティブドリブンアニメーションを出荷した最初のアプリとなりました。数か月後、Brandon Withrow氏がiOSでの初期実装を構築しました。その後、Ryan Gomba氏と私は、Animated.eventのサポートなどの不足している機能を追加したり、本番アプリで使用したときに発見したバグを修正したりする作業を行いました。これは真にコミュニティの努力であり、開発の大部分を後援してくれたExpoと同様に、関わってくれたすべての人に感謝したいと思います。現在、React NativeのTouchableコンポーネントや、新しくリリースされたReact Navigationライブラリのナビゲーションアニメーションで使用されています。

仕組みは?

まず、JSドライバーでAnimatedを使用して、アニメーションが現在どのように機能しているかを確認しましょう。Animatedを使用する場合、実行するアニメーションを表すノードのグラフを宣言し、ドライバーを使用して定義済みの曲線に従ってAnimatedの値を更新します。また、Animated.eventを使用して、Viewのイベントに接続することで、Animatedの値を更新することもできます。

アニメーションの手順と発生場所の内訳は次のとおりです。

  • JS: アニメーションドライバーは、requestAnimationFrameを使用してフレームごとに実行し、アニメーション曲線に基づいて計算した新しい値を使用して、駆動する値を更新します。
  • JS: 中間値が計算され、Viewにアタッチされているpropsノードに渡されます。
  • JS: ViewsetNativePropsを使用して更新されます。
  • JSからネイティブへのブリッジ。
  • ネイティブ: UIViewまたはandroid.Viewが更新されます。

ご覧のとおり、ほとんどの作業はJSスレッドで行われます。ブロックされると、アニメーションはフレームをスキップします。また、フレームごとにJSからネイティブへのブリッジを通過して、ネイティブビューを更新する必要があります。

ネイティブドライバーが行うことは、これらすべての手順をネイティブに移動することです。Animatedはアニメーション化されたノードのグラフを生成するため、シリアライズしてアニメーションの開始時に一度だけネイティブに送信できます。JSスレッドにコールバックする必要がなくなり、ネイティブコードはフレームごとにUIスレッドでビューを直接更新できます。

アニメーション化された値と補間ノードをシリアライズする方法の例を次に示します(正確な実装ではなく、例です)。

ネイティブ値ノードを作成します。これはアニメーション化される値です。

NativeAnimatedModule.createNode({
id: 1,
type: 'value',
initialValue: 0,
});

ネイティブ補間ノードを作成します。これは、ネイティブドライバーに値を補間する方法を指示します。

NativeAnimatedModule.createNode({
id: 2,
type: 'interpolation',
inputRange: [0, 10],
outputRange: [10, 0],
extrapolate: 'clamp',
});

ネイティブpropsノードを作成します。これは、ネイティブドライバーにアタッチされているビューのどのpropを指示します。

NativeAnimatedModule.createNode({
id: 3,
type: 'props',
properties: ['style.opacity'],
});

ノードを接続します。

NativeAnimatedModule.connectNodes(1, 2);
NativeAnimatedModule.connectNodes(2, 3);

propsノードをビューに接続します。

NativeAnimatedModule.connectToView(3, ReactNative.findNodeHandle(viewRef));

これで、ネイティブアニメーションモジュールは、JSに移動して値を計算することなく、ネイティブビューを直接更新するために必要なすべての情報を入手しました。

残っているのは、必要なアニメーション曲線の種類と更新するアニメーション化された値を指定して、アニメーションを実際に開始することだけです。タイミングアニメーションは、JSであらかじめアニメーションのすべてのフレームを計算することで、ネイティブ実装を小さくすることで簡素化することもできます。

NativeAnimatedModule.startAnimation({
type: 'timing',
frames: [0, 0.1, 0.2, 0.4, 0.65, ...],
animatedValueId: 1,
});

アニメーションの実行時に何が起こるかについての概要は次のとおりです。

  • ネイティブ: ネイティブアニメーションドライバーは、CADisplayLinkまたはandroid.view.Choreographerを使用してフレームごとに実行し、アニメーション曲線に基づいて計算した新しい値を使用して、駆動する値を更新します。
  • ネイティブ: 中間値が計算され、ネイティブビューにアタッチされているpropsノードに渡されます。
  • ネイティブ: UIViewまたはandroid.Viewが更新されます。

ご覧のとおり、JSスレッドもブリッジもなくなりました。つまり、アニメーションが高速化されます!🎉🎉

アプリでこれを使用するにはどうすればよいですか?

通常のアニメーションの場合、答えは簡単です。アニメーションの開始時にアニメーション設定にuseNativeDriver: trueを追加するだけです。

変更前

Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
}).start();

変更後

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

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

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

変更前

<ScrollView
scrollEventThrottle={16}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }]
)}
>
{content}
</ScrollView>

変更後

<Animated.ScrollView // <-- Use the Animated ScrollView wrapper
scrollEventThrottle={1} // <-- Use 1 here to make sure no events are ever missed
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }],
{ useNativeDriver: true } // <-- Add this
)}
>
{content}
</Animated.ScrollView>

注意事項

Animated でできることすべてが、現在の Native Animated でサポートされているわけではありません。主な制限は、レイアウト以外のプロパティのみをアニメーション化できることです。transformopacity のようなものは機能しますが、Flexbox や position プロパティは機能しません。もう 1 つの制限は Animated.event に関してで、直接イベントでのみ機能し、バブリングイベントでは機能しません。つまり、PanResponder では機能しませんが、ScrollView#onScroll のようなものでは機能します。

Native Animated はかなり前から React Native の一部でしたが、実験的と見なされていたため、これまでドキュメント化されていませんでした。そのため、この機能を使用する場合は、React Native の最新バージョン (0.40+) を使用していることを確認してください。

参考資料

アニメーションの詳細については、この講演 (Christopher Chedeau 氏による) をご覧になることをお勧めします。

アニメーションと、それらをネイティブにオフロードすることでユーザーエクスペリエンスをどのように向上させることができるかについて深く掘り下げたい場合は、この講演 (Krzysztof Magiera 氏による) もあります。