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

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ライブラリのナビゲーションアニメーションで使用されています。

仕組み

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

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

  • 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によるこのトークもあります。