遲早您的專案中會需要一個輪播。也許您想要顯示圖像列表,也許是您的應用程式的簡介,或者您可能希望您的應用程式有幾個可滑動的螢幕。無論您的用例是什麼,本文都可能對您有所幫助。
讓我們開始吧。我們的輪播的基礎將是一個簡單的FlatList
元件。原因很簡單 - 它基於ScrollView
元件,使我們能夠滑動幻燈片,此外,它還實現了VirtualizedList
,當幻燈片中有大量圖像或性能較高的 UI 元素時,我們可以使用 VirtualizedList 進行優化。
首先,讓我們建立一些虛擬資料。我們將使用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
屬性。我們還將向其傳遞帶有flex: 1
style
屬性,以便它覆蓋整個螢幕。最後,我們必須定義投影片的外觀。這是使用renderItem
屬性完成的。
我們將建立一個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}
屬性新增至 FlatList。
另一件事 - 我們的旋轉木馬目前是垂直的 - 它可以上下滾動。大多數旋轉木馬都是水平的,因此讓我們更改方向,但是請記住,如果您需要建造垂直旋轉木馬,這是可能的,並且只需要進行一些更改。
因此,讓我們將horizontal={true}
屬性新增至 FlatList 中,使其左右滾動,同時新增showsHorizontalScrollIndicator={false}
屬性來隱藏滾動指示器。
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}
/>
);
}
首先,我們使用useState
定義index
- 這將表示輪播中活動幻燈片的索引。然後我們定義indexRef
- 一個與索引變數保持同步的參考值 - 當index
發生變化時, indexRef.current
的值也會改變。
那我們為什麼要這樣做呢?答案在下一行。 onScroll
回呼對layoutMeasurement
和contentOffset
值進行一些計算,以便根據我們滾動的距離計算當前索引。當計算的索引發生變化時,我們希望更新我們的index
。
問題是 - 如果我們在onScroll
中使用index
變數來檢查計算的索引是否與當前索引不同,那麼我們必須將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 表示該事件每 16ms 觸發一次。我們可能可以將其設置為更高的數字,但 16 似乎工作得很好。
windowSize
- 這控制有多少投影片安裝在前面,以及有多少投影片安裝在目前索引後面。
它實際上控制 VirtualizedList 用於渲染專案的視窗的寬度 - 視窗內的所有內容都被渲染,而視窗外是空白的。例如,如果我們將此屬性設為 2,則視窗的寬度將是 FlatList 寬度的兩倍。以下視覺化中的粉紅色線表示視窗。
對於這個輪播範例,值 2 效果很好,但如果您願意,可以嘗試一下。
keyExtractor
- React 使用它進行內部優化。如果不這樣做,新增和刪除投影片可能會中斷。此外,它還刪除了警告,這很好。
getItemLayout
- 一種可選的最佳化,如果我們事先知道專案的大小(高度或寬度),則允許跳過動態內容的測量。在我們的例子中,專案的寬度始終是windowWidth
。請注意,如果您希望輪播是垂直的,則必須使用windowHeight
。
最後,我們可以將樣式移到元件定義之外,並將renderItem
函數包裝在useCallback
中,以避免 FlatList 不必要的重新渲染。
為了進一步優化我們的輪播,我們還可以做的一件事是將我們的 Slide 元素包裝在React.memo
中。
就是這樣!我加入了一個分頁元件並稍微調整了樣式,最終產品如下所示。
您可以自己嘗試:https://snack.expo.io/@hrastnik/carousel
原文出處:https://dev.to/lloyds-digital/let-s-create-a-carousel-in-react-native-4ae2