React Native Carousel 让我们在 React Native AWS Security LIVE 中创建一个轮播!

2025-06-09

React Native Carousel让我们在 React Native 中创建一个轮播

AWS 安全上线!

你的某个项目迟早会用到轮播功能。或许你想展示图片列表,或许想展示应用的入门指南,又或许想让你的应用拥有几个可滑动的屏幕。无论你的用例是什么,本文都可能对你有所帮助。

让我们开始吧。我们的轮播组件的基础将是一个简单的FlatList组件。原因很简单——它基于一个ScrollView可以让我们滑动幻灯片的组件,而且,VirtualizedList当幻灯片中有大量图片或性能要求较高的 UI 元素时,它实现了我们可以用它来进行优化的功能。

首先,让我们创建一些虚拟数据。我们将使用Lorem Picsum获取随机图像,并为轮播创建 30 张幻灯片的随机数据。



const { width: windowWidth, height: windowHeight } = Dimensions.get("window");

const slideList = Array.from({ length: 30 }).map((_, i) => {
  return {
    id: i,
    image: `https://picsum.photos/1440/2842?random=${i}`,
    title: `This is the title! ${i + 1}`,
    subtitle: `This is the subtitle ${i + 1}!`,
  };
});


Enter fullscreen mode Exit fullscreen mode

请注意,我们必须添加查询参数random=${i}来为每张幻灯片获取随机图像。否则,React Native 会缓存第一张图片,并用它代替轮播中的每一张图片。

接下来,我们将创建一个 FlatList 并将其传递slideListdataprop。我们还会将styleprop传递给它flex: 1,使其覆盖整个屏幕。最后,我们必须定义幻灯片的外观。这可以通过renderItemprop 来完成。
我们将创建一个Slide组件并在函数中使用它renderItem



function Slide({ data }) {
  return (
    <View
      style={{
        height: windowHeight,
        width: windowWidth,
        justifyContent: "center",
        alignItems: "center",
      }}
    >
      <Image
        source={{ uri: data.image }}
        style={{ width: windowWidth * 0.9, height: windowHeight * 0.9 }}
      ></Image>
      <Text style={{ fontSize: 24 }}>{data.title}</Text>
      <Text style={{ fontSize: 18 }}>{data.subtitle}</Text>
    </View>
  );
}

function Carousel() {
  return (
    <FlatList
      data={slideList}
      style={{ flex: 1 }}
      renderItem={({ item }) => {
        return <Slide data={item} />;
      }}
    />
  );
};


Enter fullscreen mode Exit fullscreen mode

垂直旋转木马,无捕捉点

如果我们现在保存,我们就能看到幻灯片了,但滚动行为并不如我们所愿。我们必须让 ScrollView 捕捉到每张幻灯片的开头。最简单的方法是将pagingEnabled={true}prop 添加到 FlatList 中。

还有一件事——我们的轮播目前是垂直的——它可以上下滚动。大多数轮播都是水平的,所以我们来改变一下方向。不过,请记住,如果你需要构建一个垂直轮播,这是可行的,只需要进行一些修改。

因此,让我们将horizontal={true}prop 添加到我们的 FlatList 中,使其左右滚动,同时,让我们添加showsHorizontalScrollIndicator={false}prop 来隐藏滚动指示器。



function Carousel() {
  return (
    <FlatList
      data={slideList}
      style={{ flex: 1 }}
      renderItem={({ item }) => {
        return <Slide data={item} />;
      }}
      pagingEnabled
      horizontal
      showsHorizontalScrollIndicator={false}
    />
  );
}


Enter fullscreen mode Exit fullscreen mode

水平旋转木马,带有捕捉点

看起来很棒,但我们忽略了一件重要的事情。我们可能需要当前幻灯片的索引。例如,如果我们正在为应用程序介绍之旅构建一个轮播,我们可能希望有一个“继续”按钮,该按钮仅在用户到达最后一张幻灯片时启用;或者,如果我们正在构建一个图片库,我们可能希望显示一个分页组件,以便让用户知道其中包含多少张图片。

我花了一些时间优化下一部分,所以看起来可能有点复杂。不过别担心,我会解释清楚一切。



function Carousel() {
  const [index, setIndex] = useState(0);
  const indexRef = useRef(index);
  indexRef.current = index;
  const onScroll = useCallback((event) => {
    const slideSize = event.nativeEvent.layoutMeasurement.width;
    const index = event.nativeEvent.contentOffset.x / slideSize;
    const roundIndex = Math.round(index);

    const distance = Math.abs(roundIndex - index);

    // Prevent one pixel triggering setIndex in the middle
    // of the transition. With this we have to scroll a bit
    // more to trigger the index change.
    const isNoMansLand = 0.4 < distance;

    if (roundIndex !== indexRef.current && !isNoMansLand) {
      setIndex(roundIndex);
    }
  }, []);

  // Use the index
  useEffect(() => {
    console.warn(index);
  }, [index]);

  return (
    <FlatList
      data={slideList}
      style={{ flex: 1 }}
      renderItem={({ item }) => {
        return <Slide data={item} />;
      }}
      pagingEnabled
      horizontal
      showsHorizontalScrollIndicator={false}
      onScroll={onScroll}
    />
  );
}


Enter fullscreen mode Exit fullscreen mode

首先,我们定义index——useState这将表示轮播中活动幻灯片的索引。然后,我们定义indexRef一个 ref 值,该值与索引变量保持同步——当 发生index变化时, 的值也会随之变化indexRef.current

那么我们为什么要这样做呢?答案就在下一行。回调函数使用和值onScroll进行了一些计算,以便根据我们滚动的距离计算出当前索引。我们希望每当计算出的索引发生变化时,就更新 。layoutMeasurementcontentOffsetindex

问题是——如果我们使用index内部变量onScroll来检查计算出的索引是否与当前索引不同,那么我们必须放入index依赖数组useCallback。这意味着每次索引发生变化时,onScroll函数也会发生变化,并且由于它作为 prop 传递给 FlatList,这意味着列表将重新渲染。

注意,由于轮播是水平的,所以我们使用了layoutMeasurement.widthcontentOffset.x来计算当前索引。如果轮播是垂直的,我们就必须使用高度和 y 偏移量。

然后是isNoMansLand变量背后的逻辑。当我们将轮播图拖到两张幻灯片的中间时,这个逻辑可以防止滑块触发一堆setIndex调用。如果我们不实现这个逻辑,就会发生这种情况——当我们位于两张幻灯片的中间时,最轻微的移动都会触发索引更改。这会导致大量的重新渲染,所以最好避免这种情况。

解决方案与此有关:施密特触发器

无施密特触发器

现在,我们目前构建的内容已经相当酷了,甚至可能足以满足您的用例,但我们的实现中存在一些隐藏的性能问题,可能会减慢您的应用速度甚至崩溃。这是因为它会提前渲染大量幻灯片,并且还会将之前的幻灯片保留在内存中。FlatList 默认会这样做,以便在快速滚动列表时提升感知性能,但在我们的例子中,这反而会对性能产生负面影响。

我编写了一个简单的可视化代码,用于显示哪些幻灯片已安装,哪些尚未安装,此外,它还突出显示了当前的索引。底部的绿点表示已安装的幻灯片,黑色点表示未安装的幻灯片,红色点表示当前活动的幻灯片。

水平轮播,未优化

您会注意到,幻灯片提前 10 张被挂载了。此外,前 10 张幻灯片从未被卸载。这都是 FlatList 默认优化的一部分,对于较长的列表来说效果很好,但不适合我们的用例。

那么让我们实现一些优化。我们将优化 props 分组到一个对象中,并将它们传递给 FlatList 。



  const flatListOptimizationProps = {
    initialNumToRender: 0,
    maxToRenderPerBatch: 1,
    removeClippedSubviews: true,
    scrollEventThrottle: 16,
    windowSize: 2,
    keyExtractor: useCallback(e => e.id, []);
    getItemLayout: useCallback(
      (_, index) => ({
        index,
        length: windowWidth,
        offset: index * windowWidth,
      }),
      []
    ),
  };

  <FlatList
    data={slideList}
    style={{ flex: 1 }}
    renderItem={({ item }) => {
      return <Slide data={item} />;
    }}
    pagingEnabled
    horizontal
    showsHorizontalScrollIndicator={false}
    onScroll={onScroll}
    {...flatListOptimizationProps}
  />


Enter fullscreen mode Exit fullscreen mode

以下是对这一切含义的解释。

initialNumToRender- 这控制从第一张幻灯片开始,有多少张幻灯片将始终保持渲染状态。这在可以通过编程滚动到顶部的列表中很有用——在这种情况下,我们不想等待前几张幻灯片渲染完毕,因此 FlatList 会始终保持渲染状态。我们不需要此功能,所以放在0这里是安全的。

maxToRenderPerBatch- 此项控制每批次渲染多少张幻灯片。当我们的 FlatList 包含许多元素,并且用户可以快速滚动到 FlatList 中尚未加载数据的区域时,此功能非常有用。

removeClippedSubviews- 这会移除 FlatLists 视口之外的视图。Android 默认将此设置为 true,我建议 iOS 也设置。它可以Image从内存中移除组件并节省一些资源。

scrollEventThrottle- 控制用户拖动轮播时触发的滚动事件数量。设置为 16 表示每 16 毫秒触发一次。我们或许可以将其设置为更高的数字,但 16 似乎效果不错。

windowSize- 这控制着有多少个幻灯片被挂载到最前面,以及有多少个幻灯片被挂载到当前索引之后。
它实际上控制着 VirtualizedList 用于渲染项目的窗口的宽度——窗口内的所有内容都会被渲染,而窗口外的内容则为空白。如果我们将此属性设置为例如 2,则窗口的宽度将是 FlatList 宽度的两倍。下图中的粉色线表示窗口。

窗口可视化

对于这个轮播示例,值 2 效果很好,但如果您愿意,可以尝试一下。

keyExtractor- React 使用它来进行内部优化。如果没有它,添加和删除幻灯片可能会中断。此外,它还移除了警告,这很好。

getItemLayout- 一项可选优化,如果我们提前知道项目的尺寸(高度或宽度),则可以跳过动态内容的测量。在我们的例子中,项目的宽度始终为windowWidth。请注意,如果您希望轮播垂直显示,则必须使用windowHeight

最后,我们可以将样式移到组件定义之外,并将renderItem函数包装进去,useCallback以避免 FlatList 不必要地重新渲染。

为了进一步优化轮播,我们可以做的另一件事是将幻灯片元素包装在 中React.memo

就这样!我添加了一个分页组件,并稍微调整了样式,最终成品如下所示。

最终轮播

您可以亲自尝试一下:https://snack.expo.io/@hrastnik/carousel

鏂囩珷鏉ユ簮锛�https://dev.to/lloyds-digital/let-s-create-a-carousel-in-react-native-4ae2
PREV
使用 MobX State Tree 规范化你的 React 查询数据
NEXT
基于 ReactJS 设计,从头构建 UI