【React Native】自定义轮播图组件 Banner

一、组件简介

Banner 是基于 react-native-pager-view 实现的高性能轮播组件,支持无限循环滚动自动播放垂直/水平方向切换自定义分页指示器等功能,适用于广告轮播、内容展示、产品推荐等场景。组件通过封装底层滚动逻辑,提供简洁的 API 接口,降低开发成本。


二、核心功能

功能描述
无限循环滚动支持首尾无缝衔接,循环展示数据(需开启 loop 属性)
自动播放自动切换轮播项(可配置延迟时间 autoplayDelay 和间隔 autoplayInterval
垂直/水平滚动支持垂直(vertical={true})或水平(默认)滚动方向
自定义分页指示器支持自定义分页点样式(颜色、大小、间距)、容器样式(背景、边距等)
手动/自动滚动控制可禁用自动播放(autoplay={false}),或通过 scrollEnabled 控制手动滚动
滚动事件回调提供 onScrollIndex(切换回调)和 onScroll(滚动过程回调)

三、属性详解(Props)

1. 基础样式与容器

属性名类型默认值描述
styleStyleProp<ViewStyle>undefined自定义 Banner 容器样式(如背景色、边距、圆角等)
verticalbooleanfalse是否垂直滚动(默认水平滚动)
scrollEnabledbooleantrue是否允许手动滚动(禁用后仅自动播放)

2. 数据与渲染

属性名类型默认值描述
dataany[] | undefinedundefined轮播数据源(必须为数组,长度需 ≥1)
renderItem(item: any, index: number) => React.ReactElementundefined渲染单个轮播项的函数(必传)
keyExtractor(item: any, index: number) => stringundefined生成唯一 key 的方法(建议提供,避免渲染警告)

3. 循环与自动播放

属性名类型默认值描述
loopbooleantrue是否开启无限循环(需 data.length ≥ 2,否则无效)
autoplaybooleantrue是否自动播放(默认开启)
autoplayDelaynumber1000自动播放前的延迟时间(毫秒,仅在首次加载时生效)
autoplayIntervalnumber5000自动切换间隔时间(毫秒)

4. 分页指示器

属性名类型默认值描述
showsPaginationbooleanfalse是否显示分页指示器(默认隐藏)
paginationStyleStyleProp<ViewStyle>undefined分页指示器容器样式(如背景色、内边距、位置等)
dotStyleStyleProp<ViewStyle>undefined普通分页点样式(如大小、颜色、间距等,与 dotColor 合并生效)
activeDotStyleStyleProp<ViewStyle>undefined当前分页点样式(如大小、颜色、边框等,与 activeDotColor 合并生效)
dotColorstring#CCCCCC普通分页点颜色(默认浅灰色)
activeDotColorstring#FFFFFF当前分页点颜色(默认白色)

5. 回调函数

属性名类型默认值描述
onScrollIndex(index: number) => voidundefined切换到指定轮播项时的回调(参数为真实数据索引)
onScroll(e: { offset: number; position: number }) => voidundefined滚动过程中的回调(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;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值