I'm using a question I asked on reddit as a reference. In the thread I was asking how to implement a copy of Spotify's snap scroll. I won't go into detail here, but I want to ask a more technical question about the behavior of FlatList in react native.
What I want to achieve is to have a Flatlist that has a "magnetic scroll" or rather called a "snap scroll" but that has a "fluid"/"normal" scroll in the header (ListHeaderComponent). What I don't understand is why if for example I use the snapToInteravall property this is also applied to the header, honestly it doesn't seem like correct behavior to me. Does anyone have any ideas on possible alternative implementations?
I tried two possible approaches without success: https://snack.expo.dev/@niccolocase/f0c9c3 https://snack.expo.dev/@niccolocase/f0c9c3
EDIT: this is the execution of the implementation of @Niclas Göransson
import React, { useRef, useState, useEffect } from "react";
import { View, Text, FlatList, Dimensions } from "react-native";
const SCREEN_WIDTH = Dimensions.get("window").width;
const SCREEN_HEIGHT = Dimensions.get("window").height;
//const HEADER_HEIGHT = SCREEN_WIDTH * 4;
const ITEM_HEIGHT = SCREEN_HEIGHT;
const ITEM_MARGIN = 10;
const DATA = Array.from({ length: 20 }, (_, index) => ({
id: index.toString(),
title: `Item ${index + 1}`,
}));
const App = () => {
const [HEADER_HEIGHT, setHEADER_HEIGHT] = useState(0);
const renderItem = ({ item, index }) => (
<View
style={{
height: ITEM_HEIGHT,
marginBottom: ITEM_MARGIN,
backgroundColor: "orange",
justifyContent: "center",
alignItems: "center",
}}
>
<Text>{item.title}</Text>
</View>
);
const renderHeader = () => (
<View
style={{
backgroundColor: "blue",
justifyContent: "center",
alignItems: "center",
}}
onLayout={(event) => {
setHEADER_HEIGHT(event.nativeEvent.layout.height);
}}
>
{new Array(200).fill(null).map((_, index) => (
<Text key={index}>{index}</Text>
))}
</View>
);
const snapOffsets = DATA.map((x, i) => {
return i * ITEM_HEIGHT + i * ITEM_MARGIN + HEADER_HEIGHT;
});
const [decelerationRate, setDecelerationRate] = useState("normal");
return (
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
ListHeaderComponent={renderHeader}
snapToStart={false}
snapToEnd={false}
snapToOffsets={snapOffsets}
decelerationRate={decelerationRate}
onScroll={(e) => {
if (e.nativeEvent.contentOffset.y >= HEADER_HEIGHT) {
setDecelerationRate(0);
} else {
setDecelerationRate("normal");
}
}}
/>
);
};
export default App;
I think the secret lies with snapToOffsets. By disabling snapToStart and snapToEnd you can specify your own snappoints and thereby skip the header.
This however is based on you knowing both header and list item height beforehand. Also seperator height if you use that.
const WINDOW_HEIGHT = Dimensions.get('window').height
const snapOffsets = flatlistData.map((x, i) => {
return (((i * ITEM_HEIGHT) + (i * ITEM_SEPERATOR_HEIGHT)) + HEADER_HEIGHT)
})
<Flatlist
data={flatlistData}
snapToStart={false}
snapToEnd={false}
snapToOffsets={snapOffsets}
/>
EDIT: Workaround to prevent scrolling past first item. Limits Flatlist height until scroll position is at first item after header.
Note that this DOES NOT work if you have other views and such besides the flatlist due to using window height.
const [tempListHeight, setTempListHeight] = useState(WINDOW_HEIGHT + HEADER_HEIGHT - StatusBar.currentHeight)
const handleScroll = (e) => {
const yScrolled = e.nativeEvent.contentOffset.y
if (yScrolled >= HEADER_HEIGHT) {
setTempListHeight((HEADER_HEIGHT - StatusBar.currentHeight) + ITEM_HEIGHT*data.length)
setDecelerationRate(0)
} else {
setTempListHeight(WINDOW_HEIGHT + HEADER_HEIGHT - StatusBar.currentHeight)
setDecelerationRate("normal")
}
}
<Flatlist
onScroll={(e) => handleScroll(e)}
contentContainerStyle={{height: tempListHeight}}
/>