一、组件简介
Banner 是基于 react-native-pager-view 实现的高性能轮播组件,支持无限循环滚动、自动播放、垂直/水平方向切换、自定义分页指示器等功能,适用于广告轮播、内容展示、产品推荐等场景。组件通过封装底层滚动逻辑,提供简洁的 API 接口,降低开发成本。
二、核心功能
| 功能 | 描述 |
|---|
| 无限循环滚动 | 支持首尾无缝衔接,循环展示数据(需开启 loop 属性) |
| 自动播放 | 自动切换轮播项(可配置延迟时间 autoplayDelay 和间隔 autoplayInterval) |
| 垂直/水平滚动 | 支持垂直(vertical={true})或水平(默认)滚动方向 |
| 自定义分页指示器 | 支持自定义分页点样式(颜色、大小、间距)、容器样式(背景、边距等) |
| 手动/自动滚动控制 | 可禁用自动播放(autoplay={false}),或通过 scrollEnabled 控制手动滚动 |
| 滚动事件回调 | 提供 onScrollIndex(切换回调)和 onScroll(滚动过程回调) |
三、属性详解(Props)
1. 基础样式与容器
| 属性名 | 类型 | 默认值 | 描述 |
|---|
style | StyleProp<ViewStyle> | undefined | 自定义 Banner 容器样式(如背景色、边距、圆角等) |
vertical | boolean | false | 是否垂直滚动(默认水平滚动) |
scrollEnabled | boolean | true | 是否允许手动滚动(禁用后仅自动播放) |
2. 数据与渲染
| 属性名 | 类型 | 默认值 | 描述 |
|---|
data | any[] | undefined | undefined | 轮播数据源(必须为数组,长度需 ≥1) |
renderItem | (item: any, index: number) => React.ReactElement | undefined | 渲染单个轮播项的函数(必传) |
keyExtractor | (item: any, index: number) => string | undefined | 生成唯一 key 的方法(建议提供,避免渲染警告) |
3. 循环与自动播放
| 属性名 | 类型 | 默认值 | 描述 |
|---|
loop | boolean | true | 是否开启无限循环(需 data.length ≥ 2,否则无效) |
autoplay | boolean | true | 是否自动播放(默认开启) |
autoplayDelay | number | 1000 | 自动播放前的延迟时间(毫秒,仅在首次加载时生效) |
autoplayInterval | number | 5000 | 自动切换间隔时间(毫秒) |
4. 分页指示器
| 属性名 | 类型 | 默认值 | 描述 |
|---|
showsPagination | boolean | false | 是否显示分页指示器(默认隐藏) |
paginationStyle | StyleProp<ViewStyle> | undefined | 分页指示器容器样式(如背景色、内边距、位置等) |
dotStyle | StyleProp<ViewStyle> | undefined | 普通分页点样式(如大小、颜色、间距等,与 dotColor 合并生效) |
activeDotStyle | StyleProp<ViewStyle> | undefined | 当前分页点样式(如大小、颜色、边框等,与 activeDotColor 合并生效) |
dotColor | string | #CCCCCC | 普通分页点颜色(默认浅灰色) |
activeDotColor | string | #FFFFFF | 当前分页点颜色(默认白色) |
5. 回调函数
| 属性名 | 类型 | 默认值 | 描述 |
|---|
onScrollIndex | (index: number) => void | undefined | 切换到指定轮播项时的回调(参数为真实数据索引) |
onScroll | (e: { offset: number; position: number }) => void | undefined | 滚动过程中的回调(offset 为偏移量,position 为当前页位置) |
四、使用示例
1. 基础用法(水平轮播)
import React from 'react';
import { View, StyleSheet } from 'react-native';
import Banner from './Banner';
const App = () => {
const data = [
{ id: 1, image: 'https://example.com/banner1.jpg' },
{ id: 2, image: 'https://example.com/banner2.jpg' },
{ id: 3, image: 'https://example.com/banner3.jpg' },
];
const renderItem = ({ item }) => (
<View style={styles.bannerItem}>
<Image source={{ uri: item.image }} style={styles.image} />
</View>
);
return (
<View style={styles.container}>
<Banner
data={data}
renderItem={renderItem}
loop={true}
autoplay={true}
autoplayInterval={3000}
showsPagination={true}
dotColor="#999"
activeDotColor="#FF5500"
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
bannerItem: {
width: '100%',
height: 200,
},
image: {
width: '100%',
height: '100%',
resizeMode: 'cover',
},
});
export default App;
2. 自定义分页指示器(垂直滚动)
<Banner
data={data}
renderItem={renderItem}
vertical={true}
loop={true}
autoplay={true}
showsPagination={true}
paginationStyle={{
backgroundColor: 'rgba(0,0,0,0.3)',
paddingHorizontal: 16,
}}
dotStyle={{
width: 6,
height: 6,
marginHorizontal: 4,
}}
activeDotStyle={{
borderWidth: 2,
borderColor: '#FFF',
}}
onScrollIndex={(index) => console.log('当前索引:', index)}
/>
五、源码
import React, {useEffect, useMemo, useRef, useState} from 'react';
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native';
import PagerView from '@react-native-oh-tpl/react-native-pager-view';
export interface Props {
style?: StyleProp<ViewStyle>;
data: any[] | undefined;
renderItem: ({item, index}: {item: any; index: number}) => React.ReactElement;
keyExtractor?: (item: any, index: number) => string;
vertical?: boolean;
loop?: boolean;
autoplay?: boolean;
autoplayDelay?: number;
autoplayInterval?: number;
showsPagination?: boolean;
paginationStyle?: StyleProp<ViewStyle>;
dotStyle?: StyleProp<ViewStyle>;
activeDotStyle?: StyleProp<ViewStyle>;
dotColor?: string;
activeDotColor?: string;
scrollEnabled?: boolean;
onScrollIndex?: (index: number) => void;
onScroll?: (e: {offset: number; position: number}) => void;
}
const Banner = ({
style,
data,
renderItem,
keyExtractor,
vertical = false,
loop = true,
autoplay = true,
autoplayDelay = 1000,
autoplayInterval = 5000,
showsPagination = false, // 默认不显示分页指示器
paginationStyle,
dotStyle,
activeDotStyle,
dotColor = '#CCCCCC', // 默认普通分页点颜色
activeDotColor = '#FFFFFF', // 默认当前分页点颜色
scrollEnabled = true,
onScrollIndex,
onScroll,
}: Props) => {
const [currentPage, setCurrentPage] = useState(0);
const [realIndex, setRealIndex] = useState(0);
const pagerRef = useRef<PagerView>(null);
const isAutoPlaying = useRef(autoplay);
const processedData = useMemo(() => {
if (!data) return [];
if (!loop || data.length <= 1) return data;
const head = [data[data.length - 1]];
const tail = [data[0]];
return [...head, ...data, ...tail];
}, [data, loop]);
const pageCount = processedData.length;
useEffect(() => {
let intervalId;
let timeoutId;
if (isAutoPlaying.current) {
timeoutId = setTimeout(() => {
intervalId = setInterval(() => {
const nextPage = currentPage === pageCount - 1 ? 0 : currentPage + 1;
pagerRef.current?.setPage(nextPage);
}, autoplayInterval);
}, autoplayDelay);
}
return () => {
clearInterval(timeoutId);
clearTimeout(intervalId);
};
}, [currentPage, pageCount, autoplayInterval, autoplayDelay]);
const pageScroll = (event: any) => {
const {offset, position} = event.nativeEvent;
onScroll?.({offset, position});
};
const pageSelected = (event: any) => {
const nextPage = event.nativeEvent.position;
setCurrentPage(nextPage);
if (loop) {
if (nextPage === 0) {
setTimeout(() => {
pagerRef.current?.setPageWithoutAnimation(processedData.length - 2);
setCurrentPage(processedData.length - 2);
setRealIndex(processedData.length - 3);
}, 100);
} else if (nextPage === processedData.length - 1) {
setTimeout(() => {
pagerRef.current?.setPageWithoutAnimation(1);
setCurrentPage(1);
setRealIndex(0);
}, 100);
} else {
onScrollIndex?.(nextPage - 1);
setRealIndex(nextPage - 1);
}
} else {
onScrollIndex?.(nextPage);
setRealIndex(nextPage);
}
};
const renderPageIndicator = () => {
const len = data?.length || 0;
if (!showsPagination || len <= 1) return null;
const dots: any[] = [];
for (let i = 0; i < len; i++) {
const isActive = i === realIndex;
const dotStyleCombined = [
styles.dot,
dotStyle,
{
backgroundColor: isActive ? activeDotColor : dotColor,
width: isActive ? 8 : 5,
height: isActive ? 8 : 5,
},
];
const activeDotStyleCombined = [
styles.activeDot,
activeDotStyle,
{
backgroundColor: activeDotColor,
width: 8,
height: 8,
},
];
dots.push(
<View
key={i}
style={isActive ? activeDotStyleCombined : dotStyleCombined}
/>,
);
}
return (
<View
style={[
{
bottom: '10%',
},
styles.pagination,
paginationStyle,
]}>
{dots}
</View>
);
};
return (
<View style={[styles.container, style]}>
<PagerView
ref={pagerRef}
style={styles.pager}
initialPage={loop ? 1 : 0}
onPageSelected={pageSelected}
onPageScroll={pageScroll}
orientation={vertical ? 'vertical' : 'horizontal'}
scrollEnabled={scrollEnabled}>
{processedData.map((item, index) => (
<View
key={keyExtractor ? `${keyExtractor(item, index)}-${index}` : index}
style={styles.page}>
{/* 渲染用户自定义内容 */}
{renderItem({item, index: realIndex})}
</View>
))}
</PagerView>
{/* 分页指示器(仅当showsPagination为true时渲染) */}
{renderPageIndicator()}
</View>
);
};
const styles = StyleSheet.create({
container: {
position: 'relative',
overflow: 'hidden',
},
pager: {
flex: 1,
},
page: {},
pagination: {
position: 'absolute',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'center',
paddingHorizontal: 10,
},
dot: {
borderRadius: 100,
marginHorizontal: 3,
},
activeDot: {
borderRadius: 100,
},
});
export default Banner;