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}!`,
};
});
请注意,我们必须添加查询参数random=${i}
来为每张幻灯片获取随机图像。否则,React Native 会缓存第一张图片,并用它代替轮播中的每一张图片。
接下来,我们将创建一个 FlatList 并将其传递slideList
给data
prop。我们还会将style
prop传递给它flex: 1
,使其覆盖整个屏幕。最后,我们必须定义幻灯片的外观。这可以通过renderItem
prop 来完成。
我们将创建一个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} />;
}}
/>
);
};
如果我们现在保存,我们就能看到幻灯片了,但滚动行为并不如我们所愿。我们必须让 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}
/>
);
}
看起来很棒,但我们忽略了一件重要的事情。我们可能需要当前幻灯片的索引。例如,如果我们正在为应用程序介绍之旅构建一个轮播,我们可能希望有一个“继续”按钮,该按钮仅在用户到达最后一张幻灯片时启用;或者,如果我们正在构建一个图片库,我们可能希望显示一个分页组件,以便让用户知道其中包含多少张图片。
我花了一些时间优化下一部分,所以看起来可能有点复杂。不过别担心,我会解释清楚一切。
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}
/>
);
}
首先,我们定义index
——useState
这将表示轮播中活动幻灯片的索引。然后,我们定义indexRef
一个 ref 值,该值与索引变量保持同步——当 发生index
变化时, 的值也会随之变化indexRef.current
。
那么我们为什么要这样做呢?答案就在下一行。回调函数使用和值onScroll
进行了一些计算,以便根据我们滚动的距离计算出当前索引。我们希望每当计算出的索引发生变化时,就更新 。layoutMeasurement
contentOffset
index
问题是——如果我们使用index
内部变量onScroll
来检查计算出的索引是否与当前索引不同,那么我们必须放入index
依赖数组useCallback
。这意味着每次索引发生变化时,onScroll
函数也会发生变化,并且由于它作为 prop 传递给 FlatList,这意味着列表将重新渲染。
注意,由于轮播是水平的,所以我们使用了layoutMeasurement.width
和contentOffset.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}
/>
以下是对这一切含义的解释。
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