前言
本文主要记录在 ReactNative 里如何实现 ScrollView 及 WebView 的上下拖动切换。
由于 ReactNative 里 WebView 没有提供 onScrollEndDrag
等拖动事件的回调,所以只能通过别的方法来实现。原生的 WebView 有这些回调,但是这样的话得借助 iOS 跟 android 两端的原生代码,这里我们只通过 js 来实现最终效果:
PS:截止本文时间,RN 最新版本为 0.48.0,下面的 demo 是以 0.48.0 为基础的
方案
上面使用 ScrollView 来承载内容,这个是没有问题的,关键是对下面 WebView 的处理。
RN 的 WebView 可以通过 postMessage
onMessage
来跟网页进行交互,所以我们可以通过给 WebView 注入一些 js 代码来实现一些交互,有两种方案:
- 将 WebView 用一个 ScrollView 包裹,然后给 WebView 注入一段 js 得到网页内容高度,之后再传回 RN 端来改变 WebView 高度。结构大概如下:
1 2 3 4 5 6 7 8
| <Animated.View> <ScrollView> { /* your contents here */ } </ScrollView> <ScrollView> <WebView /> </ScrollView> </Animated.View>
|
- 给 WebView 注入一段 js 代码,在网页端来监听触摸事件(
touchstart
、touchmove
、touchend
),通过统计 touchmove
事件 在顶部继续下拉 被调用的次数,在拖动结束后将结果传回给 RN 端处理。结构大概如下:
1 2 3 4 5 6
| <Animated.View> <ScrollView> { /* your contents here */ } </ScrollView> <WebView /> </Animated.View>
|
第一种方案有个问题,就是如果网页本身有个一直停留在顶部的 header 的话(即样式为 position: static
)(如上面 gif 图中网页顶部的推荐、视频、娱乐、体育、时尚
那一栏),改变 webview 高度的话,会导致这个 header 跟着一起滑动了;
第二种方案在 小于 5.0 的安卓系统上行不通,因为系统原因,WebView 不能实时监听到 touchmove
事件。
所以综合起来,解决方案如下:
- <5.0 的 android 系统,使用方案一
- iOS 系统及 ≥5.0 的 android 系统,使用方案二
编码
通过监听 ScrollView 的 onScrollEndDrag
事件,然后通过最外层的 Animated.View
来进行切换即可。
其中由于 iOS 有弹性效果,即到了顶部/底部后还是可以继续拖动,但是 android 是不行的,所以在 onScrollEndDrag
里,需要对 Y 值的位移(offsetY
)做一下不同判断。
其中 iOS 判断到顶部后继续下拉超过 60(可自行修改),android 判断距离 ≥ -1(因为最小为 0)就触发切换动作,这里比较简单,代码大概如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <Animated.View style={{ height: onePartHeight * 2, transform: [{ translateY: this.state.moveValue }] }}> <ScrollView style={styles.scrollView} onScrollEndDrag={(e) => { const contentSizeH = e.nativeEvent.contentSize.height const offsetY = e.nativeEvent.contentOffset.y if (offsetY - (contentSizeH - onePartHeight) >= (Platform.OS === 'ios' ? 60 : -1)) { Animated.timing(this.state.moveValue, { toValue: -onePartHeight }).start() } }} > <View style={styles.scrollContentBox}> <Text>scrollView's top</Text> <Text>scrollView's center</Text> <Text>scrollView's bottom (has paddingBottom down here)</Text> </View> </ScrollView> { Platform.OS === 'android' && Platform.Version < 21 // 21 为 5.0 系统 ? {/* 方案一,详见下文 */} : {/* 方案二,详见下文 */} } </Animated.View>
|
这里我们对 WebView 进行一下封装(下面以 SCWebView
为名进行描述),主要做两件事:
- 分别为两个方案注入不同的 js
- 实现
onMessage
,监听网页端传过来的参数
查看 WebView 文档, 通过 injectedJavaScript
即可注入 js,通过 onMessage
即可监听网页端传过来的参数,render 方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| render () { const jsCode = this._injectJSString() return ( <View style={[styles.box, this.props.boxStyle]}> <WebView ref={web => (this._webView = web)} style={[styles.webView, this.props.style, { height: this.props.autoHeight ? this.state.webViewHeight : this.props.style.height }]} source={this.props.source || { uri: this.props.url }} javaScriptEnabled domStorageEnabled mixedContentMode={'always'} scalesPageToFit injectedJavaScript={(jsCode)} onMessage={(event) => this._onMessage(event)} /> </View> ) }
|
其中 _injectJSString
根据不同方案注入不同的 js:
1 2 3 4 5 6 7 8 9 10 11 12
| _injectJSString () { var str = this._injectPostMsgJS() if (this.props.autoHeight) { // 方案一 str += this._injectAutoHeightJS() } if (this.props.scrollToTop) { // 方案二 str += this._injectScrollToTopJS() } return str }
|
方案一注入 js 去获取网页内容高度后通过 postMessage
方法传给 RN 端,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| _injectAutoHeightJS () { if (!this.props.autoHeight) { return '' } const getHeightFunc = function () { let height = 0 if (document.documentElement.clientHeight > document.body.clientHeight) { height = document.documentElement.clientHeight } else { height = document.body.clientHeight } var action = { type: 'changeWebviewHeight', params: { height: height } } window.postMessage(JSON.stringify(action)) } const str = '(' + String(getHeightFunc) + ')();' return str }
|
方案二注入 js 让网页端监听 touch 事件,判断到达顶部后,touchmove
事件调用超过 10 次(数值可自行修改),就通过 postMessage
方法告诉 RN 端触发切换事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| _injectScrollToTopJS () { if (!this.props.scrollToTop) { return '' } const onScrollToTopFunc = function () { var sysVersion = -1 var _userAgent = navigator.userAgent if (/iPad|iPhone|iPod/.test(_userAgent) && !window.MSStream) { sysVersion = 0 // iOS } else { var match = _userAgent.toLowerCase().match(/android\s([0-9\\.]*)/) sysVersion = match ? parseFloat(match[1]) : -1 } var good = !!((sysVersion === 0 || (sysVersion !== -1 && sysVersion >= 5.0))) if (good) { // 只监听 iOS 以及 android 5.0+系统(因为 android 4.x 系统的 touchmove 事件不能实时监听) var count = 0 window.addEventListener('touchstart', function (event) { count = 0 }, false) window.addEventListener('touchmove', function (event) { // console.log(document.body.scrollTop) document.body.scrollTop > 0 ? count = 0 : count++ }, false) window.addEventListener('touchend', function (event) { if (count >= 10) { const action = { type: 'scrollToTop' } window.postMessage(JSON.stringify(action)) } count = 0 }, false) } } const str = '(' + String(onScrollToTopFunc) + ')();' return str }
|
最后我们通过 onMessage
方法去处理网页端传过来的参数,这里网页端调用 postMessage
传过来的参数只能是字符串,所以我们定义一下简单的规则:
- 网页端传过来的参数为 JSON 字符串
- JSON 字符串通过
type
字段表明不同事件
- 其它参数通过
params
字段组合
如网页端这样使用:
1 2
| var action = { type: 'changeWebviewHeight', params: { height: height } } window.postMessage(JSON.stringify(action))
|
RN 端监听如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| _onMessage (event) { try { const data = JSON.parse(event.nativeEvent.data) if (!data.type) { return } const params = data.params switch (data.type) { case 'scrollToTop': if (this.props.scrollToTop) { this.props.scrollToTop() } break case 'changeWebviewHeight': this.setState({ webViewHeight: params.height }) break default: break } } catch (error) { console.warn('webview onMessage error: ' + error.message) } }
|
总结
Demo 代码放上 GitHub 了,可去 https://github.com/Aevit/SCRNDemo 查看这两个文件:
2017-09-21 21:36
Aevit
深圳南山
摄影:Aevit 2015年11月 华师