[ReactNative] ScreenView+Webview refreshControlについての備忘録(Androidのバグ)

最近小さめのReactNativeのプロジェクトにアサインされたワタクシでございます。
全くまだ分かってないんですけど、UI部分の改修案件が来たので対応しています。
その際に直面したバグ対応の備忘録を残します。

## どのようなバグか
iosでは起きず、Androidのみのバグとなります。

```javascript
render() {
  <ScrollView
     refreshControl={
       <RefreshControl
         refreshing={ this.state.refreshing }
         style={{ backgroundColor: "#FFFFFF" }}
         onRefresh={() => { this.onRefresh() }}
       />
     }
     contentContainerStyle={{flex: 1}}
     style={{ backgroundColor: "#FFFFFF" }}
   >
            <WebView
              style={{ flex: 1 }}
              onMessage={this.onMessage}
              onNavigationStateChange={this.onNavigationStateChange}
              onLoadStart={this.onLoadStart}
              onLoadEnd={this.onLoadEnd}
              ref={webView => {this.webView = webView}}
              source={{ uri: this.state.url.toString() }}
            />
    </ScrollView>
}
```
このようなソースがあります。
webviewを表示しており、このwebviewはスクロールが可能になっています。
スクロールをする際にiosでは起きませんが、Androidではスクロールで下までいってしまったあとに上に戻ろうとすると必ずrefreshControlが走り、上にスクロールが出来なくなるバグが起きてました。
アプリの上部に来た時だけ更新してくれればいいのに、常に上部をひっぱると更新されてしまいます。

## 原因
webview側は問題なくスクロールします。スクロールされているのはScrollViewではなくて、webviewであることがわかり、(ScrollViewにonScrollつけてもイベントが走らなかったため)ScrollViewのscrollYが更新されず0のままでした。
そうすると恐らく、scrollYが0であるとしてAndroidでは常にrefreshControlが走ってしまうのではないか、というところまでたどり着きました。

## 開発環境
* react-native: 0.61.5
* react: 16.9.5
* react-native-webview: 8.0.3

## 解決方法
```javascript
const INJECTED_JS = `
  window.onscroll = function() {
    window.ReactNativeWebView.postMessage(
      JSON.stringify({
        scrollTop: document.documentElement.scrollTop || document.body.scrollTop
      }),
    )
`

export default class WebViewPage extends React.Component {
  state = {
    scrollViewHeight: 0,
    isPullToRefreshEnabled: false,
  }

  onMessage = () => {
    const { data } = e.nativeEvent

    try {
      const { scrollTop } = JSON.parse(data)
      this.setState({ isPullToRefreshEnabled: scrollTop === 0 })
    } catch (err) {
      // ...
    }
  }

  render () {
    const { scrollViewHeight, isPullToRefreshEnabled } = this.state

    return (
      <View style={{flex:1}}>
        <ScrollView
          refreshControl={
            <RefreshControl
              refreshing={ this.state.refreshing }
              enabled={ isPullToRefreshEnabled }
              style={{ backgroundColor: "#FFFFFF" }}
              onRefresh={() => { this.onRefresh() }}
            />
          }
          onLayout={e => this.setState({ scrollViewHeight: e.nativeEvent.layout.height })}
          contentContainerStyle={{flex: 1}}
          style={{ backgroundColor: "#FFFFFF" }}
        >
          <WebView
            style={{ flex: 1 }}
            onMessage={this.onMessage}
            onNavigationStateChange={this.onNavigationStateChange}
            onLoadStart={this.onLoadStart}
            onLoadEnd={this.onLoadEnd}
            ref={webView => {this.webView = webView}}
            source={{ uri: this.state.url.toString() }}
            injectedJavaScript={INJECTED_JS}
          />
        </ScrollView>
      </View>
    )
  }
}
```

## 解説
1. ScrollViewでスクロールイベント取れないならwebviewで取るしかないよなということで `injectedJavaScript` を導入。injectedJavaScriptはwebview側にjavascriptのコードを渡す属性で、ここでwebviewのスクロール位置を取得して、`window.ReactNativeWebView.postMessage` でReactNative側にその情報を渡してやります。この時、webview側にもonMessageメソッドを定義して、webview側のスクロール位置情報を受け取ります
2. enabledでいつrefreshControlが走るか定義しておく(onMessage)
3. ScrollViewコンポーネントのonLayoutを使って、viewが開いたときの高さを取得して保持しておきます。(初回だけ取得用)
4. WebviewコンポーネントにinjectedJavaScriptを突っ込みます。

以上です。これで画面の一番上にスクロールした時にだけ上部を引っ張ればrefreshControlが走る設計になったかと思います。

## 参考文献
* https://github.com/react-native-webview/react-native-webview/issues/103
* https://github.com/facebook/react-native/issues/19476
* https://qiita.com/ozaki25/items/9da8e313e026f447c421