React Native 带进度条横向滚动 本篇参照
我是用 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>
);
};