はじめに
今回、react-native-modalというライブラリを使ってモーダルを実装した際に、モーダルを閉じるアニメーションで背景にチラつきが発生する(下の動画参照)ことがあったため、その対応をまとめました。今回の解決策は、他のアニメーションの実装でも有用だと思うので、タイトルは広義な表現をしています。
追記
2021/12/13
react-native-modalでモーダルを閉じた際にチラつきが発生する問題について、後述している内容だけで治らなかった場合は、以下のプロパティを追加してください。
hideModalContentWhileAnimating={true}
解決方法
コンポーネントに対して、useNativeDriver
というpropsを追加します。(useNativeDriver={true}
も可)
<Modal
isVisible={isOpen}
onBackdropPress={onClose}
+ useNativeDriver>
/** contents */
</Modal>
これによって、チラつきがなくなり綺麗なアニメーションになります。
useNativeDriver
って?
解決方法は至ってシンプルでしたが、そもそもuseNativeDriver
がなにものか知らなかったので調査して見ました。
React NativeのUsing Native Driver for Animated という記事に以下の記述がありました。
The Animated API was designed with a very important constraint in mind, it is serializable. This means we can send everything about the animation to native before it has even started and allows native code to perform the animation on the UI thread without having to go through the bridge on every frame. It is very useful because once the animation has started, the JS thread can be blocked and the animation will still run smoothly.
なるほど、全てのフレームでJSブリッジを通過させずにネイティブコードを実行することで、アニメーションのパフォーマンスを向上させることができるみたいです。
となると、そもそもアニメーションがどう動いているかですが、それについても言及されていました。
従来のアニメーションのフロー
- アニメーションドライバは
requestAnimationFrame
を用いて全てのフレームで実行され、アニメーション曲線に基づいて計算された新しい値を使用して、アニメーションの値を更新します - 中間値が計算され、
View
コンポーネントにアタッチされているprops
ノードに渡します -
View
はsetNativeProps
を用いて更新します - JavaScriptからネイティブブリッジに処理が移ります
-
UIView
もしくはandroid.View
が更新されます
※ Using Native Driver for Animatedより引用
1.~ 3.の処理が JavaScriptのスレッド上で実行されていること、毎フレームでJSからネイティブブリッジに通過させる必要があることの2点が、ビューを更新する上で重い処理となっているようで、JavaScriptによる実行が他の処理などによってブロックされた場合アニメーションがそのフレームでスキップされてしまうようです。
上記の問題はこの辺りが原因なのでしょうか??
そこで、パフォーマンスを改善する方法として、JSスレッドで行われていた部分をネイティブで行う方法が考えられました。
JavaScriptを利用しないアニメーションのフロー
- ネイティブのアニメーションドライバは
CADisplayLink
もしくはandroid.view.Choreographer
を用いて全てのフレームで実行され、アニメーション曲線に基づいて計算された新しい値を使用して、アニメーションの値を更新します - 中間値が計算され、ネイティブのビューにアタッチされている
props
ノードに渡します -
UIView
もしくはandroid.View
が更新されます
JavaScriptのスレッド上で実行する必要のある処理は一切なく、そのためJSからネイティブブリッジへの移動も無くなったため、アニメーションが高速になりました!詳しい内部処理は依然わからないところはありますが、概要はつかめたのではないかと思います。
上記のフローに変更するために必要なのが、useNativeDriver
を追加することというわけです。
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
+ useNativeDriver: true, // <-- Add this
}).start();
対応しているアニメーションのプロパティ
記事の最後に、以下の記載がありました。
Not everything you can do with Animated is currently supported in Native Animated. The main limitation is that you can only animate non-layout properties, things like transform and opacity will work but Flexbox and position properties won't. Another one is with Animated.event, it will only work with direct events and not bubbling events. This means it does not work with PanResponder but does work with things like ScrollView#onScroll.
全てのアニメーションがネイティブアニメーション(つまりJavaScriptを用いないアニメーション)に対応しているわけではなく、transform
やopacity
といったプロパティでは機能しますが、FlexBoxやposition
では機能しないようです。(ブログで currently と書かれており、その時のReact Nativeの最新バージョンはv0.65(2021/10/01時))また、Animated.event
に関しては直接的なイベント(Scrollイベントなど)は機能しますが、バブリングするイベント(PanResponderイベントなど)では機能しないようです。まだ制約もあるのでuseNativeDriver
のデフォルト値をfalse
にしている、という感じでしょうか。
また、この機能はまだ実験段階であるようなので、React Nativeのバージョンが0.40以上で利用できるようです。React Nativeのv0.40.0
のリリースが2017年1月4日とかなり前なので特に心配ないと思いますが、、
最後に
React Nativeではアニメーションのパフォーマンス改善に、JavaScriptを用いずにアニメーションを実行する方法があることがわかりました。今ではReact Nativeの標準のコンポーネントであるTouchable
コンポーネント(TouchableHighlight
やTouchableOpacity
など)やReact Navigation
というルーティングを管理するライブラリのアニメーションでも用いられているようです。
利用できる機能であればuseNativeDriver
を有効にする、という考えで間違いないかと思います。そのうち全てのアニメーションで利用できるようになればいいですね