美文网首页
RN中的TabView和Swiper手势冲突之初探解决方案

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

作者: Hozan | 来源:发表于2021-03-11 17:14 被阅读0次

    抛出问题:

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

    参考链接

    相关文章

      网友评论

          本文标题:RN中的TabView和Swiper手势冲突之初探解决方案

          本文链接:https://www.haomeiwen.com/subject/nlyrzktx.html