抛出问题:
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。
探索解决方案:
1、牺牲TabView 的滑动切换功能,TabView有一个swipeEnabled属性,设置为false禁止滑动切换TabView。当然这种用户体验不好。
2、能否通过swipeEnabled的开启与禁用来解决滚动冲突?怎么监听手指触摸的是哪块视图?带着问题我们来看下源码:
- 打开react-native-swiper/src/index.js文件,轮播图的滑动主要是通过ScrollView来实现的,如下这段代码:
renderScrollView = pages => {
return (
{pages}
)
}
- 打开react-native-tab-view/src/TabView.tsx文件,从106-213行代码是主要的实现,代码有点长,这里就不贴了。
顺着代码下来能够找到Pager.tsx文件,如下贴个核心代码:
{children}
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(
)
}
}
export function SwiperView(props) {
const { data, onChage } = props
// console.log(onChage);
return (
{
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 (
)
})
}
)
}
按照以上代码的效果变好了一点,不过出现个问题: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 (
{
console.log(tab.i)
}}
locked={false}
style={{}}
tabBarActiveTextColor={theme.content_color}
tabBarInactiveTextColor={'#646566'}
tabBarTextStyle={{ fontSize: 14 }}>
);
}
}
const SwiperView = class extends Component {
render() {
return (
swiper1
swiper2
swiper3
)
}
}
const styles = StyleSheet.create({
mainStyle: {
flex: 1
},
})
抛出问题:为什么这种情况下没有出现手势冲突了?
来看看两个库的核心源码:
- react-native-scrollable-tab-view/index.js
renderScrollableContent() {
if (Platform.OS === 'ios') {
const scenes = this._composeScenes();
return
{scenes}
;
} else {
const scenes = this._composeScenes();
return { this.scrollView = scrollView; }}
{...this.props.contentProps}
>
{scenes}
;
}
},
- react-native-swiper/src/index.js
renderScrollView = pages => {
if (Platform.OS === 'ios') {
return (
{pages}
)
}
return (
{pages}
)
}
你会发现两个库的核心组件都是用ViewPagerAndroid,所以你可以大胆猜测下:ViewPagerAndroid在手势监听方面做了一些操作,当你使用两个ViewPagerAndroid相互嵌套的话,根据手势或者触摸哪个View来决定滚动哪个。感兴趣的可以去看下ViewPagerAndroid的源代码。
就好像两个ScrollView相互嵌套,如果你不通过手势监听去禁止另外一个不滚动的话,那就会导致其中有个ScrollView滚动不了。
我还做了一个实验,就是把else部分的代码注释掉,统一用成ScrollView,结果是手势冲突了,切换Swiper的时候会出现切换成Tab。
对Tab和Swiper的手势冲突的初探就到这了,后面有空继续探究。
参考链接
- 手势冲突踩坑
- react-native-gesture-handler官方文档