react-native页面滑动时添加顶部悬浮Navbar、添加悬浮组件跟随某个组件位置,
效果如图:
110_1702611282 (1).gif
关键代码如下:
//1、使用Animated.FlatList的onScroll事件
<Animated.FlatList
style={{ flex: 1 }}
contentContainerStyle={{
paddingBottom: ScreenUtils.safeBottom,
backgroundColor: '#fff',
}}
showsVerticalScrollIndicator={false}
data={dataList}
ListHeaderComponent={this.renderHeader}
renderItem={this.renderItem}
onRefresh={this.onRefresh}
onEndReached={this.onEndReached}
refreshing={refreshing}
keyExtractor={(__, k) => `${k}`}
ListEmptyComponent={loadEnd && <Footer noType={"no_hot_product"} />}
ListFooterComponent={isLoadMore && <ListFooter />}
onScroll={Animated.event([
{
nativeEvent: { contentOffset: { y: this.scrollY } }
}], { useNativeDriver: false })
}
/>
//2、添加需要悬浮显示组件,其中“headerHeight - 90 - ScreenUtils.headerHeight”,90是两个tab的组件高度,ScreenUtils.headerHeight是状态栏和导航栏的高度。
<Animated.View
style={{
position: 'absolute',
top: 0,
opacity: this.scrollY.interpolate({
inputRange: [-1, 0, 60],
outputRange: [0, 0, 1],
})
}}>
<NavBar backgroundColor={"#fff"} hideBottomLine={true}>{"标题"}</NavBar>
{headerHeight > 0 && <Animated.View
style={{
width: ScreenUtils.width,
position: 'absolute',
backgroundColor: '#fff',
opacity: this.scrollY.interpolate({
inputRange: [0, headerHeight - 90 - ScreenUtils.headerHeight, headerHeight - 90 - ScreenUtils.headerHeight + 1],
outputRange: [0, 0, 1],
}),
top: this.scrollY.interpolate({
inputRange: [0, headerHeight - 90 - ScreenUtils.headerHeight, headerHeight - 90 - ScreenUtils.headerHeight + 1],
outputRange: [-120, ScreenUtils.headerHeight, ScreenUtils.headerHeight],
})
}}>
{this.renderTabView({ marginTop: 0 })}
</Animated.View>}
</Animated.View>
{this.renderFloatModal()}
注意,因为悬浮组件和固定组件是两个相同组件,如果在tab组件上添加滑动效果的时候要注意让两个tab都跟着滑动:
firstTabScrollViewRef1 = React.createRef();
firstTabScrollViewRef2 = React.createRef();
scrollToIndex = (index) => {
const layout = this.layoutList[index];
if (!layout) {
return;
}
if (this.firstTabScrollViewRef1.current && this.firstTabScrollViewRef2.current) {
//先算出tab的中心点,再算出scrollview实际需要滑动的距离(scrollview左边的left位置-屏幕中间位置=实际超出屏幕的位置)
const left = layout.x + (layout.width) / 2;
const x = left - (ScreenUtils.width / 2);
this.firstTabScrollViewRef1.current.scrollTo({ x: x, animated: true });
this.firstTabScrollViewRef2.current.scrollTo({ x: x, animated: true });
}
}
{this.renderTabView(this.firstTabScrollViewRef1)}
{this.renderTabView(this.firstTabScrollViewRef2, { marginTop: 0 })}
以下是该页面全部代码,引用的组件可以自行替换后测试:
import React from "react";
import {
View,
StyleSheet,
Text,
ImageBackground,
Animated,
ScrollView,
} from "react-native";
import { Footer, NavBar, } from "../../../component/All";
import { I18n } from '@lang';
import { UIButton, UIButtonWithImage, UIButtonWithSingleText } from "../../../component/UIButton";
import ScreenUtils from "../../../utils/ScreenUtils";
import ListFooter from "../../../component/ListFooter";
import { ListItemView } from "./component/ListItemView";
/**
* 会场样式三,顶部有图片、标题、两个tab显示(第一个tab显示类目,第二个tab是日月周榜)
*/
export default class MeetPlaceThirdPage extends React.Component {
scrollY = new Animated.Value(0);
topHeight = new Animated.Value(0);
hasMore = false;
params = {
page: 1,//页数
limit: 20,//每页的条数
type: 1,//类型 0:总榜 1:日榜 2:周榜 3:月榜
};
constructor(props) {
super(props);
this.state = {
firstTabIndex: 0,
firstTabList: [{ name: "综合" },
{ name: "商品类目一" },
{ name: "商品类目二" },
{ name: "商品类目二" },
{ name: "商品类目二" },
{ name: "商品类目二" },
{ name: "商品类目二" },
],
secondTabIndex: 0,
secondTabList: [I18n.t('day'), I18n.t('week'), I18n.t('all')],
headerHeight: 0,//用于固定tab高度
showFloatModal: false,//是否显示悬浮的类目弹窗
refreshing: false,
dataList: [0, 1, 1, 1, 1, , 11, 1, 1, 1, 1, 1, 1,],
loadEnd: false,// 是否首次加载完毕
isLoadMore: false,//是否加载更多
}
}
componentDidMount() {
this.onRefresh(false)
}
/**
* @param {*} parentId
*/
getCategoryList = async (parentId = 0) => {
const params = { parentId };//parentId 父级分类编号 0:一级分类;
let list = [];
try {
list = await api_goods_sub_type_list(params);
const newList = [{ name: I18n.t('all') }].concat(list);
this.setState({ firstTabList: newList });
} catch (error) {
}
}
scrollToIndex = (index) => {
const layout = this.layoutList[index];
if (!layout) {
return;
}
if (this.firstTabScrollViewRef1.current && this.firstTabScrollViewRef2.current) {
//先算出tab的中心点,再算出scrollview实际需要滑动的距离(scrollview左边的left位置-屏幕中间位置=实际超出屏幕的位置)
const left = layout.x + (layout.width) / 2;
const x = left - (ScreenUtils.width / 2);
this.firstTabScrollViewRef1.current.scrollTo({ x: x, animated: true });
this.firstTabScrollViewRef2.current.scrollTo({ x: x, animated: true });
}
}
render() {
const { refreshing, dataList, loadEnd, isLoadMore, headerHeight } = this.state;
return <View style={styles.container}>
<Animated.FlatList
style={{ flex: 1 }}
contentContainerStyle={{
paddingBottom: ScreenUtils.safeBottom,
backgroundColor: '#fff',
}}
showsVerticalScrollIndicator={false}
data={dataList}
ListHeaderComponent={this.renderHeader}
renderItem={this.renderItem}
onRefresh={this.onRefresh}
onEndReached={this.onEndReached}
refreshing={refreshing}
keyExtractor={(__, k) => `${k}`}
ListEmptyComponent={loadEnd && <Footer noType={"no_hot_product"} />}
ListFooterComponent={isLoadMore && <ListFooter />}
onScroll={Animated.event([
{
nativeEvent: { contentOffset: { y: this.scrollY } }
}], { useNativeDriver: false })
} />
<Animated.View
style={{
position: 'absolute',
top: 0,
opacity: this.scrollY.interpolate({
inputRange: [-1, 0, 60],
outputRange: [0, 0, 1],
})
}}>
<NavBar backgroundColor={"#fff"} hideBottomLine={true}>{"标题"}</NavBar>
{headerHeight > 0 && <Animated.View
style={{
width: ScreenUtils.width,
position: 'absolute',
backgroundColor: '#fff',
opacity: this.scrollY.interpolate({
inputRange: [0, headerHeight - 90 - ScreenUtils.headerHeight, headerHeight - 90 - ScreenUtils.headerHeight + 1],
outputRange: [0, 0, 1],
}),
top: this.scrollY.interpolate({
inputRange: [0, headerHeight - 90 - ScreenUtils.headerHeight, headerHeight - 90 - ScreenUtils.headerHeight + 1],
outputRange: [-120, ScreenUtils.headerHeight, ScreenUtils.headerHeight],
})
}}>
{this.renderTabView(this.firstTabScrollViewRef2, { marginTop: 0 })}
</Animated.View>}
</Animated.View>
{this.renderFloatModal()}
</View>
}
//类目悬浮窗
renderFloatModal = () => {
const { firstTabList, firstTabIndex, showFloatModal, headerHeight } = this.state;
if (!showFloatModal) {
return null;
}
return <Animated.View
style={{
flex: 1,
position: 'absolute',
top: this.scrollY.interpolate({
inputRange: [0, headerHeight - 90 - ScreenUtils.headerHeight, headerHeight - 90 - ScreenUtils.headerHeight + 1],
outputRange: [headerHeight - 50, ScreenUtils.headerHeight + 40, ScreenUtils.headerHeight + 40],
}),
left: 0,
right: 0,
bottom: 0,
}}>
<ScrollView style={{
height: ScreenUtils.height / 3,
maxHeight: ScreenUtils.height / 3,
backgroundColor: '#fff'
}}>
<View style={styles.floatContainer}>
{firstTabList.map((item, index) => {
const isSelected = (firstTabIndex == index);
return <UIButtonWithSingleText key={index}
btnStyle={{
backgroundColor: isSelected ? '#FFE7EC' : '#F7F8F9',
borderRadius: 20,
paddingHorizontal: 13,
paddingVertical: 6,
marginBottom: 12,
marginRight: 16
}}
text={item.name}
textStyle={{ color: isSelected ? '#FA0C43' : '#666', fontSize: 12 }}
onPress={() => {
this.scrollToIndex(index);
this.setState({
firstTabIndex: index,
showFloatModal: false
});
}
} />
})}
</View>
</ScrollView>
<UIButton style={{ flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.30)' }}
onPress={() => this.setState({ showFloatModal: false })} />
</Animated.View>
}
renderHeader = () => {
return <View
onLayout={(event) => {
this.setState({ headerHeight: (event.nativeEvent?.layout?.height ?? 0) })
}}>
<ImageBackground source={require("@assets/home/homePage/bset_follow_bg.png")}
style={{
width: ScreenUtils.width,
height: ScreenUtils.width / 1.6
}}>
<NavBar whiteBack={true}
backgroundColor={"transparent"}
hideBottomLine={true}>{"标题"}</NavBar>
<View style={{ flex: 1 }}>
<Text style={styles.title}>爆品榜单</Text>
<Text style={styles.otherTitle}>副文案副文案副文案</Text>
</View>
</ImageBackground>
{this.renderTabView(this.firstTabScrollViewRef1)}
</View>
}
renderTabView = (ref, style) => {
const { firstTabList, firstTabIndex, secondTabList, secondTabIndex, showFloatModal } = this.state;
return <View style={[{ marginTop: -12 }, style]}>
<View style={styles.tabContainer}>
<ScrollView
ref={ref}
horizontal={true}
showsHorizontalScrollIndicator={false}
style={styles.firstTabContainer}>
{firstTabList.map((item, index) => {
const isSelected = (firstTabIndex == index);
return <TouchableOpacity style={styles.tabItemView} key={index}
onPress={() => this.setState({ firstTabIndex: index })}
onLayout={(event) => {
this.layoutList[index] = event.nativeEvent.layout;
}}>
<Text style={{ color: isSelected ? '#000' : '#B3B3B3', fontSize: 14 }}>{item.name}</Text>
{isSelected && <View style={styles.tabItemLine} />}
</TouchableOpacity>
})}
</ScrollView>
<UIButtonWithImage
source={require("@assets/meetplace/more_icon.png")}
imageStyle={{
width: 16,
height: 15,
marginHorizontal: 13,
transform: [{ rotate: showFloatModal ? '-90deg' : '0deg' }]
}}
btnStyle={{ height: 40, width: 42 }}
onPress={() => {
const show = !showFloatModal;
this.setState({ showFloatModal: show })
}} />
</View>
<View style={{ flexDirection: 'row', marginLeft: 16, marginVertical: 12 }}>
{secondTabList.map((item, index) => {
const isSelected = (secondTabIndex == index);
return <UIButton key={index}
style={[styles.tabSecondItemView, { borderColor: isSelected ? '#FA0C43' : '#CDCDCD' }]}
onPress={() => {
this.setState({ secondTabIndex: index }, () => {
this.onRefresh(false);
})
}}>
<Text style={{ color: isSelected ? '#FA0C43' : '#000', fontSize: 12 }}>{item}</Text>
</UIButton>
})}
</View>
</View>
}
renderItem = ({ item, index }) => {
return <ListItemView item={item}
style={{ marginTop: index == 0 ? 0 : 12 }}
navigation={this.props.navigation} />
}
getData() {
const { secondTabIndex } = this.state;
//类型 0:总榜 1:日榜 2:周榜 3:月榜
switch (secondTabIndex) {
case 0:
this.params.type = 1;
break;
case 1:
this.params.type = 2;
break;
case 2:
this.params.type = 0;
break;
}
api_goods_hot_rank(this.params).then((res) => {
this.setState({ refreshing: false, loadEnd: true, isLoadMore: false });
const rows = res.list;
this.hasMore = this.params.page < res.totalPage;
let newData = rows;
if (this.params.page > 1) {
newData = this.state.dataList.concat(rows);
}
this.setState({ dataList: newData });
}).catch((error) => {
this.setState({ refreshing: false, loadEnd: true, isLoadMore: false });
})
}
onRefresh = (refreshing = true) => {
this.params.page = 1;
this.setState({ refreshing }, () => {
this.getData()
})
}
onEndReached = () => {
if (this.hasMore) {
this.params.page += 1;
this.setState({ isLoadMore: true })
this.getData();
}
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#F9F9F9",
},
title: {
color: '#fff',
fontSize: 28,
fontWeight: '800',
marginTop: 20,
marginLeft: 20
},
otherTitle: {
color: '#fff',
fontSize: 14,
fontWeight: '600',
marginLeft: 20,
marginTop: 4
},
tabContainer: {
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
backgroundColor: '#fff',
height: 40,
flexDirection: 'row',
alignItems: 'center',
borderBottomColor: '#ECECEC',
borderBottomWidth: 1
},
firstTabContainer: {
flexDirection: 'row',
flex: 1,
height: 40,
maxHeight: 40,
paddingHorizontal: 4,
},
tabItemView: {
paddingHorizontal: 12,
alignItems: 'center',
justifyContent: 'center',
height: 40
},
tabItemLine: {
backgroundColor: '#000',
height: 2,
borderRadius: 6,
width: 32,
position: 'absolute',
bottom: 0
},
floatContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
backgroundColor: '#fff',
paddingLeft: 16,
paddingTop: 16,
paddingBottom: 4
}
});
网友评论