抛出问题:
react-native-tab-view中的TabView和react-native-swiper的Swiper滚动冲突,版本如下:
"react-native-swiper": "^1.5.14",
"react-native-tab-view": "^2.15.2",
注意:加上这个 ^ 貌似会向上自动升级,所以实际上可能下载的是github上最新的版本。
以上两个库是在"react-native": "0.63.4"的版本下测试的。
假设我要实现如下图所示功能,TabView嵌套了轮播图,滚动轮播图和切换TabView互不干扰,但事与愿违,滚动轮播图会导致切换了Tab。
示例图.jpg
探索解决方案:
1、牺牲TabView 的滑动切换功能,TabView有一个swipeEnabled属性,设置为false禁止滑动切换TabView。当然这种用户体验不好。
<TabView
style={{}}
navigationState={{ index, routes }}
swipeEnabled={false}
renderScene={renderScene}/>
2、能否通过swipeEnabled的开启与禁用来解决滚动冲突?怎么监听手指触摸的是哪块视图?带着问题我们来看下源码:
- 打开react-native-swiper/src/index.js文件,轮播图的滑动主要是通过ScrollView来实现的,如下这段代码:
renderScrollView = pages => {
return (
<ScrollView
ref={this.refScrollView}
{...this.props}
{...this.scrollViewPropOverrides()}
contentContainerStyle={[styles.wrapperIOS, this.props.style]}
contentOffset={this.state.offset}
onScrollBeginDrag={this.onScrollBegin}
onMomentumScrollEnd={this.onScrollEnd}
onScrollEndDrag={this.onScrollEndDrag}
style={this.props.scrollViewStyle}
>
{pages}
</ScrollView>
)
}
- 打开react-native-tab-view/src/TabView.tsx文件,从106-213行代码是主要的实现,代码有点长,这里就不贴了。
顺着代码下来能够找到Pager.tsx文件,如下贴个核心代码:
<PanGestureHandler
ref={this.gestureHandlerRef}
simultaneousHandlers={this.state.childPanGestureHandlerRefs}
waitFor={this.state.childPanGestureHandlerRefs}
enabled={layout.width !== 0 && swipeEnabled && this.state.enabled}
onGestureEvent={this.handleGestureEvent}
onHandlerStateChange={this.handleGestureEvent}
activeOffsetX={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
failOffsetY={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
{...gestureHandlerProps}
>
<Animated.View
removeClippedSubviews={removeClippedSubviews}
style={[
styles.container,
layout.width
? {
width: layout.width * navigationState.routes.length,
transform: [{ translateX }] as any,
}
: null,
]}
>
<PagerContext.Provider value={this.providerVal}>
{children}
</PagerContext.Provider>
</Animated.View>
</PanGestureHandler>
TabView的滑动切换功能是通过 'react-native-gesture-handler'的PanGestureHandler来实现的,通过enabled来控制是否能切换Tab。
- 尝试解决冲突,代码如下(这里展示不是完整代码,只为了说明):
import { TabView, TabBar, SceneMap } from 'react-native-tab-view'
export class HomeScreen extends Component {
constructor(props) {
super(props)
this.state = {
index: 0,
routes: [
{ key: 'first', title: '猜你喜欢', onChage: this.onChage },
{ key: 'second', title: '今日特价', onChage: this.onChage },
{ key: 'third', title: '发现好店', onChage: this.onChage }
],
swipeEnabled: true
}
}
onChage = (e) => {
console.log(e);
if (e == 'onTouchStartCapture' ) {
if(this.state.swipeEnabled){
this.setState({
swipeEnabled: false
})
}
} else {
if(!this.state.swipeEnabled){
this.setState({
swipeEnabled: true
})
}
}
}
render() {
return(
<TabView
style={{}}
navigationState={{ index, routes }}
swipeEnabled={this.state.swipeEnabled}
renderScene={renderScene}/> )
}
}
export function SwiperView(props) {
const { data, onChage } = props
// console.log(onChage);
return (
<Swiper
onTouchStartCapture={(e) => {
onChage && onChage('onTouchStartCapture')
}}
onMomentumScrollEnd={(e)=>{
onChage && onChage('onMomentumScrollEnd')
}}
style={{
height: 230,
backgroundColor: colors.bgColorfa,
paddingHorizontal: 10,
}}
paginationStyle={{ bottom: 5 }}
loop={false}
dotStyle={{ backgroundColor: colors.dotunsel }}
activeDotStyle={{ backgroundColor: colors.theme }}>
{
data.map((arrData, index) => {
return (
<View key={index}>
</View>
)
})
}
</Swiper>
)
}
按照以上代码的效果变好了一点,不过出现个问题:onMomentumScrollEnd概率性没回调,如下所示:
LOG onTouchStartCapture -- swipeEnabled=false
LOG onMomentumScrollEnd -- swipeEnabled=true
LOG onTouchStartCapture -- swipeEnabled?false
这个时候TabView进行了切换,也就是说当onMomentumScrollEnd回调完成,onTouchStartCapture也回调了,但是this.setState()的时候swipeEnabled还没变成false,导致Tab切换了,所以onMomentumScrollEnd就没有回调。
综上所述:当你手速比较快地不停切换,通过swipeEnabled的禁用和开启还是概率性出现手势冲突问题。此方案不可行。
3、能不能通过官方提供的Panresponder来解决手势冲突呢?毕竟它有提供onPanResponderGrant、onPanResponderRelease、onPanResponderTerminate这些方法让我们去做一些操作。按道理TabView和Swiper的实质都是两个滚动View,我们可以监听手势是否放在Swiper上,从而禁止TabView的滚动;当手势完全释放的时候,两个View就都开启滚动。
这种思路后面会着重研究下,有空再写一篇博文分享。
4、RN还有一个比较好用的TabView组件react-native-scrollable-tab-view,在满足以下两种情况,TabView和Swiper就不会冲突了:
- 安装以下版本:
"react-native": "0.57.7",
"react-native-scrollable-tab-view": "0.10.0",
"react-native-swiper": "1.5.14",
注意要固定版本号,不要加^
你可能会问:react-native为啥不用上0.60+的版本?因为新的react-native版本去掉了ViewPagerAndroid,把它抽离出一个单独的库(@react-native-community/viewpager)。而react-native-scrollable-tab-view和react-native-swiper的旧版本都用上了react-native提供的ViewPagerAndroid。
你可能会问:不能升级这两个库到最新版本来适配RN的最新版本吗?你去试试就知道了,报错是肯定有的。@react-native-community/viewpager的最新版本用ts写的,估计项目要支持ts才行,或者去找一个js版本的试试。
- Swiper的loop要设置为true,也就是轮播滚动循环,如果为false,则当你滚动到最后一个的时候,再滑动就是切换Tab了。
这里演示个例子,代码如下所示:
import React, { Component } from 'react'
import {
StyleSheet,
View,
Text
} from 'react-native'
import NavbarView from '../../component/public/navbarView/navbarViewWhite'
import theme from '../../common/theme';
import ScrollableTabView from 'react-native-scrollable-tab-view';
import Swiper from 'react-native-swiper'
export default class TestDemo extends Component {
render() {
return (
<View style={styles.mainStyle}>
<NavbarView title={'TabView嵌套Swiper'} />
<ScrollableTabView
onChangeTab={(tab) => {
console.log(tab.i)
}}
locked={false}
style={{}}
tabBarActiveTextColor={theme.content_color}
tabBarInactiveTextColor={'#646566'}
tabBarTextStyle={{ fontSize: 14 }}>
<View tabLabel={'tab1'} style={{ flex: 1 }}>
<SwiperView />
<View style={{ flex: 1 }}></View>
</View>
<View tabLabel={'tab2'} style={{ flex: 1 }}>
<SwiperView />
<View style={{ flex: 1 }}></View>
</View>
<View tabLabel={'tab3'} style={{ flex: 1 }}>
<SwiperView />
<View style={{ flex: 1 }}></View>
</View>
</ScrollableTabView>
</View>
);
}
}
const SwiperView = class extends Component {
render() {
return (
<View style={{ height: 200 }}>
<Swiper
style={{}}
paginationStyle={{ bottom: 0 }}
loop={true}
dotStyle={{ backgroundColor: theme.window_color }}
activeDotStyle={{ backgroundColor: 'blue' }}>
<View style={{ flex: 1, backgroundColor: 'red' }}>
<Text>swiper1</Text>
</View>
<View style={{ flex: 1, backgroundColor: 'green' }}>
<Text>swiper2</Text>
</View>
<View style={{ flex: 1, backgroundColor: 'yellow' }}>
<Text>swiper3</Text>
</View>
</Swiper>
</View>
)
}
}
const styles = StyleSheet.create({
mainStyle: {
flex: 1
},
})
抛出问题:为什么这种情况下没有出现手势冲突了?
来看看两个库的核心源码:
- react-native-scrollable-tab-view/index.js
renderScrollableContent() {
if (Platform.OS === 'ios') {
const scenes = this._composeScenes();
return <Animated.ScrollView
...
>
{scenes}
</Animated.ScrollView>;
} else {
const scenes = this._composeScenes();
return <AnimatedViewPagerAndroid
key={this._children().length}
style={styles.scrollableContentAndroid}
initialPage={this.props.initialPage}
onPageSelected={this._updateSelectedPage}
keyboardDismissMode="on-drag"
scrollEnabled={!this.props.locked}
onPageScroll={Animated.event(
[{
nativeEvent: {
position: this.state.positionAndroid,
offset: this.state.offsetAndroid,
},
}, ],
{
useNativeDriver: true,
listener: this._onScroll,
},
)}
ref={(scrollView) => { this.scrollView = scrollView; }}
{...this.props.contentProps}
>
{scenes}
</AnimatedViewPagerAndroid>;
}
},
- react-native-swiper/src/index.js
renderScrollView = pages => {
if (Platform.OS === 'ios') {
return (
<ScrollView ref={this.refScrollView}
... >
{pages}
</ScrollView>
)
}
return (
<ViewPagerAndroid ref={this.refScrollView}
{...this.props}
initialPage={this.props.loop ? this.state.index + 1 : this.state.index}
onPageScrollStateChanged={this.onPageScrollStateChanged}
onPageSelected={this.onScrollEnd}
key={pages.length}
style={[styles.wrapperAndroid, this.props.style]}>
{pages}
</ViewPagerAndroid>
)
}
你会发现两个库的核心组件都是用ViewPagerAndroid,所以你可以大胆猜测下:ViewPagerAndroid在手势监听方面做了一些操作,当你使用两个ViewPagerAndroid相互嵌套的话,根据手势或者触摸哪个View来决定滚动哪个。感兴趣的可以去看下ViewPagerAndroid的源代码。
就好像两个ScrollView相互嵌套,如果你不通过手势监听去禁止另外一个不滚动的话,那就会导致其中有个ScrollView滚动不了。
我还做了一个实验,就是把else部分的代码注释掉,统一用成ScrollView,结果是手势冲突了,切换Swiper的时候会出现切换成Tab。
对Tab和Swiper的手势冲突的初探就到这了,后面有空继续探究。
网友评论