[React Native]アニメーションでちらつきが発生した時の対処法

# はじめに
今回、[react-native-modal](https://github.com/react-native-modal/react-native-modal)というライブラリを使ってモーダルを実装した際に、モーダルを閉じるアニメーションで背景にチラつきが発生する（下の動画参照）ことがあったため、その対応をまとめました。今回の解決策は、他のアニメーションの実装でも有用だと思うので、タイトルは広義な表現をしています。

![bad.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405522/8fa5fe46-1eee-dd2c-370d-17b3340f06ea.gif)

# 追記
## 2021/12/13
[react-native-modal](https://github.com/react-native-modal/react-native-modal)でモーダルを閉じた際にチラつきが発生する問題について、後述している内容だけで治らなかった場合は、以下のプロパティを追加してください。

```js
hideModalContentWhileAnimating={true}
```


# 解決方法

コンポーネントに対して、`useNativeDriver`というpropsを追加します。（`useNativeDriver={true}`も可）


```js
<Modal
  isVisible={isOpen}
  onBackdropPress={onClose}
+ useNativeDriver>
 /** contents */
</Modal>
```

これによって、チラつきがなくなり綺麗なアニメーションになります。

![good.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405522/46e086cf-3639-bbc2-a2a2-93a3674a72a9.gif)

# `useNativeDriver`って？

解決方法は至ってシンプルでしたが、そもそも`useNativeDriver`がなにものか知らなかったので調査して見ました。
React Nativeの[Using Native Driver for Animated](https://reactnative.dev/blog/2017/02/14/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ブリッジを通過させずにネイティブコードを実行することで、アニメーションのパフォーマンスを向上させることができるみたいです。
となると、そもそもアニメーションがどう動いているかですが、それについても言及されていました。

## 従来のアニメーションのフロー

![スクリーンショット 2021-10-01 11.10.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405522/48aecfdf-c2f2-e446-f1f5-1dc5c8877e37.png)

1. アニメーションドライバは`requestAnimationFrame`を用いて全てのフレームで実行され、アニメーション曲線に基づいて計算された新しい値を使用して、アニメーションの値を更新します
1. 中間値が計算され、`View`コンポーネントにアタッチされている`props`ノードに渡します
1. `View`は`setNativeProps`を用いて更新します
1. JavaScriptからネイティブブリッジに処理が移ります
1. `UIView`もしくは`android.View`が更新されます

※ [Using Native Driver for Animated](https://reactnative.dev/blog/2017/02/14/using-native-driver-for-animated)より引用

1.~ 3.の処理が JavaScriptのスレッド上で実行されていること、毎フレームでJSからネイティブブリッジに通過させる必要があることの２点が、ビューを更新する上で重い処理となっているようで、JavaScriptによる実行が他の処理などによってブロックされた場合アニメーションがそのフレームでスキップされてしまうようです。

上記の問題はこの辺りが原因なのでしょうか？？

そこで、パフォーマンスを改善する方法として、JSスレッドで行われていた部分をネイティブで行う方法が考えられました。

# JavaScriptを利用しないアニメーションのフロー

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

JavaScriptのスレッド上で実行する必要のある処理は一切なく、そのためJSからネイティブブリッジへの移動も無くなったため、アニメーションが高速になりました！詳しい内部処理は依然わからないところはありますが、概要はつかめたのではないかと思います。

上記のフローに変更するために必要なのが、`useNativeDriver`を追加することというわけです。

```js
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日とかなり前なので特に心配ないと思いますが、、:sweat_smile:

# 最後に

React Nativeではアニメーションのパフォーマンス改善に、JavaScriptを用いずにアニメーションを実行する方法があることがわかりました。今ではReact Nativeの標準のコンポーネントである`Touchable`コンポーネント（`TouchableHighlight`や`TouchableOpacity`など）や`React Navigation`というルーティングを管理するライブラリのアニメーションでも用いられているようです。

利用できる機能であれば`useNativeDriver`を有効にする、という考えで間違いないかと思います。そのうち全てのアニメーションで利用できるようになればいいですね:blush: