最近小さめのReactNativeのプロジェクトにアサインされたワタクシでございます。
全くまだ分かってないんですけど、UI部分の改修案件が来たので対応しています。
その際に直面したバグ対応の備忘録を残します。
どのようなバグか
iosでは起きず、Androidのみのバグとなります。
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
解決方法
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>
)
}
}
解説
- ScrollViewでスクロールイベント取れないならwebviewで取るしかないよなということで
injectedJavaScript
を導入。injectedJavaScriptはwebview側にjavascriptのコードを渡す属性で、ここでwebviewのスクロール位置を取得して、window.ReactNativeWebView.postMessage
でReactNative側にその情報を渡してやります。この時、webview側にもonMessageメソッドを定義して、webview側のスクロール位置情報を受け取ります - enabledでいつrefreshControlが走るか定義しておく(onMessage)
- ScrollViewコンポーネントのonLayoutを使って、viewが開いたときの高さを取得して保持しておきます。(初回だけ取得用)
- WebviewコンポーネントにinjectedJavaScriptを突っ込みます。
以上です。これで画面の一番上にスクロールした時にだけ上部を引っ張ればrefreshControlが走る設計になったかと思います。