一、组件简介
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; // 生成唯一 key 的方法(建议提供)
vertical?: boolean; // 是否垂直滚动(默认水平滚动)
loop?: boolean; // 是否开启无限循环(默认关闭)
autoplay?: boolean; // 是否自动播放(默认关闭)
autoplayDelay?: number; // 自动播放延迟时间(毫秒)
autoplayInterval?: number; // 自动切换间隔时间(毫秒)
showsPagination?: boolean; // 是否显示分页指示器(默认false)
paginationStyle?: StyleProp<ViewStyle>; // 分页指示器容器样式
dotStyle?: StyleProp<ViewStyle>; // 普通分页点样式
activeDotStyle?: StyleProp<ViewStyle>; // 当前分页点样式
dotColor?: string; // 普通分页点颜色(默认#CCCCCC)
activeDotColor?: string; // 当前分页点颜色(默认#FFFFFF)
scrollEnabled?: boolean; // 是否允许手动滚动(默认true)
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;