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

Animatedでネイティブドライバーを使用する

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

この1年間、私たちは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を使用して、Animated値をViewのイベントに接続して更新することもできます。

以下に、アニメーションの手順とそれがどこで発生するかを示します。

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

ご覧の通り、ほとんどの作業はJSスレッドで行われます。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',
});

ネイティブプロップスノードを作成します。これは、ネイティブドライバーに、接続されているビューのどのプロップを対象とするかを指示します。

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

ノードを接続する

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

プロップノードをビューに接続する

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を使用して毎フレーム実行され、アニメーションカーブに基づいて計算された新しい値を使用して、駆動する値を更新します。
  • ネイティブ: 中間値が計算され、ネイティブビューにアタッチされたプロップノードに渡されます。
  • ネイティブ: 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や位置プロパティは機能しません。もう1つはAnimated.eventに関するもので、これは直接イベントでのみ機能し、バブリングイベントでは機能しません。これは、PanResponderでは機能しないが、ScrollView#onScrollのようなものでは機能することを意味します。

Native AnimatedはReact Nativeの一部としてかなり前から存在していましたが、実験的とみなされていたため、文書化されていませんでした。そのため、この機能を使用したい場合は、React Nativeの新しいバージョン(0.40以降)を使用していることを確認してください。

リソース

アニメーションに関する詳細については、Christopher Chedeauによるこの講演を視聴することをお勧めします。

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