RN中的TabView和Swiper手势冲突之初探解决方案

2021-03-11  本文已影响0人  Hozan

抛出问题:

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的开启与禁用来解决滚动冲突?怎么监听手指触摸的是哪块视图?带着问题我们来看下源码:

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>
    )
  }
<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版本的试试。

这里演示个例子,代码如下所示:

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
    },
})

抛出问题:为什么这种情况下没有出现手势冲突了?

来看看两个库的核心源码:

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>;
    }
  },
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的手势冲突的初探就到这了,后面有空继续探究。

参考链接

上一篇下一篇

猜你喜欢

热点阅读