react-native实现多列表左右滑动+滚动TabBar悬停
用到的组件
rn版本:0.70.6
"react-native-tab-view": "3.5.2",
页面效果
初始状态:

滚动后:

左滑动到右边列表状态:

实现思路
使用react-native-tab-view实现左右切换,但是不能使用renderTabBar,因为tabBar会始终显示在列表顶部,不能跟着列表进行滚动,顶部蓝色区域+红色tabbar总高度400(tabbar高度50),然后通过Animated动画+FlatList的onScroll设置动画的值,让顶部区域网屏幕外移出或移入,当滚动超过350(总高度400-tabbar高度50)就将已经准备好的另一个tabbar显示出来,这样就可以实现这样的滚动悬停效果
完整实现代码,可直接使用,需要集成前置组件react-native-tab-view
javascript
import React, {useRef, useState, useCallback} from 'react';
import {View, Animated, Easing, Dimensions, Text} from 'react-native';
import {TabView, SceneMap, TabBar} from 'react-native-tab-view';
const {width} = Dimensions.get('window');
const ChatFlatList = () => {
const [tabId, setTabId] = useState(0);
const [routes] = useState([
{key: 'tab1', title: 'Tab 1'},
{key: 'tab2', title: 'Tab 2'},
]);
const [sy1, setScrollY1] = useState(0);
const [sy2, setScrollY2] = useState(0);
const listRef1 = useRef<any>(null);
const listRef2 = useRef<any>(null);
const [tabBarVisible, setTabBarVisible] = useState(false);
const scrollY = useRef(new Animated.Value(0)).current;
// 顶部 View 的 translateY
const headerTranslateY = scrollY.interpolate({
inputRange: [0, 400],
outputRange: [0, -400],
extrapolate: 'clamp',
});
const tabViewMarginTop = scrollY.interpolate({
inputRange: [0, 400],
outputRange: [400, 0], // 当 scrollY=0 时 marginTop=400;当 scrollY=400 时 marginTop=0
extrapolate: 'clamp',
});
const renderTabContent = useCallback(
(data: any, tid: number) => (
<Animated.FlatList
ref={tid == 1 ? listRef1 : listRef2}
data={data}
onScroll={Animated.event(
[{nativeEvent: {contentOffset: {y: scrollY}}}],
{
useNativeDriver: true,
// ✅ 安全地监听滚动,不影响性能
listener: (event: any) => {
const y = event.nativeEvent.contentOffset.y;
setTabBarVisible(y >= 350);
if (tid == 1) {
setScrollY1(y);
} else {
setScrollY2(y);
}
},
},
)}
scrollEventThrottle={16}
renderItem={({item, index}) => (
<View
style={[
{height: 60, backgroundColor: '#f0f0f0', marginVertical: 2},
index == 0 && {marginTop: 400},
]}>
<Text>Item {item}</Text>
</View>
)}
keyExtractor={item => item.toString()}
/>
),
[scrollY],
);
const renderScene = useCallback(
SceneMap({
tab1: () => renderTabContent([...Array(50).keys()], 1),
tab2: () => renderTabContent([...Array(30).keys()], 2),
}),
[],
);
return (
<>
{/* 顶部可滚动的 View */}
<Animated.View
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 400,
backgroundColor: 'skyblue',
zIndex: 1,
transform: [{translateY: headerTranslateY}],
}}>
{/* <Text style={{fontSize: 20, marginTop: 200, textAlign: 'center'}}>
滚动时会上移
</Text> */}
<View style={{height: 350, width: '100%'}}></View>
<View
style={{height: 50, width: '100%', backgroundColor: 'red'}}></View>
</Animated.View>
<View
style={{
position: 'absolute',
top: 0,
left: 0,
height: 50,
width: '100%',
backgroundColor: 'red',
zIndex: 10,
opacity: tabBarVisible ? 1 : 0,
}}></View>
{/* TabView */}
<TabView
navigationState={{index: tabId, routes}}
renderScene={renderScene}
onIndexChange={(index: number) => {
console.log('index', index, 'sy1', sy1, 'sy2', sy2);
//切换到当前第一个列表
if (index == 0) {
//如果第二个列表滚动位置小于400,滚动到第二个列表的位置
if (sy2 < 400) {
listRef1?.current?.scrollToOffset({offset: sy2, animated: false});
} else if (sy1 < 400) {
//第二个列表滚动超过400,需要判断第一个列表滚动位置是否小于400,如果小于400则滚动到400,否则不动
listRef1?.current?.scrollToOffset({offset: 400, animated: false});
}
} else if (index == 1) {
//切换到当前第二个列表
//如果第一个列表滚动位置小于400,滚动到第一个列表的位置
if (sy1 < 400) {
listRef2?.current?.scrollToOffset({offset: sy1, animated: false});
} else if (sy2 < 400) {
//第一个列表滚动超过400,需要判断第二个列表滚动位置是否小于400,如果小于400则滚动到400,否则不动
listRef2?.current?.scrollToOffset({offset: 400, animated: false});
}
}
setTabId(index);
}}
initialLayout={{width}}
renderTabBar={props => null}
/>
</>
);
};
export default ChatFlatList;