React Native ScrollView实现 带进度条横向滚动(类似淘宝的菜单横向滚动)
React Native 带进度条横向滚动 本篇参照 https://blog.csdn.net/JJochen/article/details/112544264

我是用 React Hooks写的 这里贴出要注意的地方:
(1)重新计算 marLeftAnimated 时,监听ScrollView的滚动事件。如果滚动了把一个自定义量的值改变,只要这个值改变了,就说明滚动了,滚动就重新计算 marLeftAnimated



防止别人的路径删了。我这里把我的代码放上来。
index.tsx
// 社区Tab下 协会Tab页 import React, { ReactNode, useEffect, useState } from 'react'; import { ImageSourcePropType } from 'react-native'; import { connect } from 'react-redux'; import { View, Colors, Text, Image } from 'react-native-ui-lib'; import { DispatchPro, RootState } from '../../../store'; import { FlatList, LineSpace, Touchable, WidthSpace } from '../../../components'; import { handleAssociationList } from './config'; import { IndicatorScrollView } from '../components/indicatorScrollView'; import { deviceWidth, navigate } from '../../../utils'; import { IDaynamicItem } from '../../../models/community/ICommunity.module'; import DaynamicItem from '../components/daynamicItem'; import configs from '../../../configs'; type IAssociation = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>; const Association = (props: IAssociation) => { const { fetchAssociationList, fetchAssociationDaynamicList, associationList } = props; const columnLimit = 5; //option列数量 const rowLimit = 2; //option行数量 const [daynamicList, setDaynamicList] = useState<IDaynamicItem[]>([]); // 动态列表数据 const [currentPageNo, setCurrentPageNo] = useState(0); // 动态列表分页 // 页面初始化时,请求数据 const initRequest = () => { fetchAssociationList({ params: { }, apiName: 'fetchAssociationList', }); }; useEffect(() => { initRequest(); }, []); // 请求动态列表数据,距离底部还有0.2 时触发 注意此参数是一个比值而非像素单位。比如,0.5 表示距离内容最底部的距离为当前列表可见长度的一半时触发。 const fetchDataFn = React.useMemo(() => new Promise((resolve) => { fetchAssociationDaynamicList && fetchAssociationDaynamicList({ params: { pageNo: currentPageNo + 1, pageSize: 10, pageTypeId: '2', associationId: '0' }, apiName: 'fetchDaynamicList', }).then(res => { setDaynamicList([...daynamicList, ...(res?.shareInfoList || [])]); resolve({ pageNo: currentPageNo + 1, totalCount: res?.totalCount, results: [...daynamicList, ...(res?.shareInfoList || [])] }); }); }), [currentPageNo]); const pageToNextFn = () => { setCurrentPageNo(currentPageNo + 1); }; // 点击单个协会,跳转至协会详情页 const onPress = (id: string, title: string) => { navigate('Webview', { url: `${configs.reactUrl}/associationDetail?associationId=${id}`, title, }); }; // 横向滚动协会里面的内容 option const renderOption = () => { const size = (deviceWidth - 20) / columnLimit; // 每个option的宽度 const optionTotalArr: ReactNode[] = []; //存放所有option样式的数组 //根据行数,声明用于存放每一行渲染内容的数组 for (let i = 0; i < rowLimit; i++) optionTotalArr.push([]); handleAssociationList(associationList).map((item, index) => { let rowIndex = 0; //行标识 if (index < columnLimit * rowLimit) { //没超出一屏数量时,根据列数更新行标识 rowIndex = parseInt(String(index / columnLimit)); } else { //当超出一屏数量时,根据行数更新行标识 rowIndex = index % rowLimit; } (optionTotalArr[rowIndex] as ReactNode[]).push( <Touchable key={index} onPress={() => onPress(item.linkParams, item.text)} activeOpacity={0.7} style={{ marginTop: 21, justifyContent: 'center', alignItems: 'center' }} > <View style={{ width: size, alignItems: 'center', justifyContent: 'center' }}> <Image style={{ width: 36, height: 36, borderRadius: 18 }} source={item.image as ImageSourcePropType} /> <LineSpace height={6} /> <Text l21>{item.text}</Text> </View> </Touchable>, ); }); return ( <View style={{ flex: 1, justifyContent: 'center', paddingHorizontal: 10 }} > { optionTotalArr.map((item: ReactNode, index: number) => { return <View key={index} style={{ flexDirection: 'row' }}>{item}</View>; }) } </View> ); }; return ( <FlatList fetchDataFn={fetchDataFn} pageToNextFn={pageToNextFn} triggerPageNo={currentPageNo} keyExtractor={(item: IDaynamicItem) => item.shareId} renderItem={({ item }: { item: IDaynamicItem }) => { return <View> <LineSpace height={15} /> <View row> <WidthSpace width={15} /> <DaynamicItem data={item} /> </View> </View>; }} ListHeaderComponent={<> <View style={{ alignItems: 'center' }} > <IndicatorScrollView containerStyle={{ flex: 1, width: (deviceWidth - 20) / columnLimit }} indicatorBgStyle={{ marginBottom: 10, borderRadius: 2, width: 30, height: 4, backgroundColor: Colors.greyDD }} indicatorStyle={{ borderRadius: 2, height: 4, backgroundColor: Colors.primaryColor }} > {renderOption()} </IndicatorScrollView> </View > </>} /> ); }; const mapStateToProps = ({ community: { associationList, associationDaynamicList, associationDaynamicLoading }, }: RootState) => ({ associationList, associationDaynamicList, associationDaynamicLoading }); const mapDispatchToProps = ({ community: { fetchAssociationList, fetchAssociationDaynamicList }, }: DispatchPro) => ({ fetchAssociationList, fetchAssociationDaynamicList, }); export default connect(mapStateToProps, mapDispatchToProps)(Association);
IndicatorScrollView .tsx
import React, { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react';
import { View, ScrollView, Animated } from 'react-native';
import { Colors } from 'react-native-ui-lib';
import { LineSpace } from '../../../../components';
import { deviceWidth } from '../../../../utils';
type IProps = {
children: ReactNode; // 展示的内容
containerStyle: CSSProperties; // 容器样式
indicatorBgStyle: CSSProperties; //滚动条框样式
indicatorStyle: CSSProperties; //滚动条样式
};
// 协会 横向滚动,根据自己定义的列数和行数,确定展示的数据
export const IndicatorScrollView = (props: IProps) => {
const defaultProps = {
containerStyle: { width: deviceWidth, backgroundColor: Colors.white },
style: {},
indicatorBgStyle: {
marginBottom: 10,
borderRadius: 2,
width: 30,
height: 4,
backgroundColor: Colors.greyDD,
},
indicatorStyle: {
borderRadius: 2,
height: 4,
backgroundColor: Colors.primaryColor,
},
};
//滑动偏移量
const scrollOffset = new Animated.Value(0);
//显示滑动进度部分条的长度
const [barWidth, setBarWidth] = useState(defaultProps.indicatorBgStyle.width / 2);
const [childWidth, setChildWidth] = useState(defaultProps.containerStyle.width); //ScrollView子布局宽度
const [scrollMark, setScrollMark] = useState(0); //设置协会横向滚动的标识,值变了说明滚动了,滚动了就重新计算marLeftAnimated
const marLeftAnimated: React.MutableRefObject<any> = useRef(); //蓝色滚动条距离左边的位置、
// 横向滚动触发
const animatedEvent = Animated.event([
{
nativeEvent: {
contentOffset: { x: scrollOffset },
},
},
]);
useEffect(() => {
//内容可滑动距离
const scrollDistance = childWidth - defaultProps.containerStyle.width;
if (scrollDistance > 0) {
const _barWidth =
(defaultProps.indicatorBgStyle.width * defaultProps.containerStyle.width) / childWidth;
setBarWidth(_barWidth);
//显示滑动进度部分的距左距离
const leftDistance = defaultProps.indicatorBgStyle.width - _barWidth;
const newscrollOffset = scrollOffset;
marLeftAnimated.current = newscrollOffset.interpolate({
inputRange: [0, scrollDistance], //输入值区间为内容可滑动距离
outputRange: [0, leftDistance], //映射输出区间为进度部分可改变距离
extrapolate: 'clamp', // 绑定动画值到指定插值范围
});
}
}, [scrollMark, childWidth]);
return (
<View style={{ flex: 1, ...defaultProps.containerStyle }}>
<ScrollView
horizontal={true} //横向
alwaysBounceVertical={false}
alwaysBounceHorizontal={false}
showsHorizontalScrollIndicator={false} //自定义滑动进度条,所以这里设置不显示
scrollEventThrottle={0.1} //滑动监听调用频率
onScroll={(e) => { animatedEvent(e); setScrollMark(scrollMark + 1); }} //滑动监听事件,用来映射动画值
scrollEnabled={childWidth - defaultProps.containerStyle.width > 0 ? true : false}
onContentSizeChange={(width) => {
if (childWidth != width) {
setChildWidth(width);
}
}}
>
{props.children ?? <View style={{ flexDirection: 'row' }}>{props.children}</View>}
</ScrollView>
{childWidth - defaultProps.containerStyle.width > 0 ? (
<>
<LineSpace height={15} />
<View style={[{ alignSelf: 'center' }, defaultProps.indicatorBgStyle]}>
<Animated.View
style={{
position: 'absolute',
width: barWidth,
top: 0,
left: marLeftAnimated.current,
...defaultProps.indicatorStyle,
}}
/>
</View>
<LineSpace height={14} />
</>
) : null}
</View>
);
};
浙公网安备 33010602011771号