I have a component with a horizontal <FlatList>
that contains 100s of items (calendar days), with the same width as the screen. The <FlatList>
is set to snap to an interval similar to the screen width, so I can swipe between items.
When I swipe to a specific date, I want to update the UI to show that date, above the <FlatList>
. Right now I'm using onMomentumScrollEnd
to find the final offset and the day item to show in the header.
However, the user experience is not optimal, since the header is not updated before the swiping/scrolling animation ends.
What I need is is a way to determine which offset that the <FlatList>
will snap to, as soon as the user swipes.
I guess onScroll
could be useful, but I'm a little lost on finding the next offset hat the <FlatList>
will snap to, based on the event I get from onScroll
.
However, I can see that some 3rd party modules like react-native-snap-carousel
is doing this, using onScroll
, so I guess it's possible.
I have chosen not to use react-native-snap-carousel
as it really decreases the swiping animation performance on my screen. And looking into the code if the modul gives no clear answer, since this module has tons of features that I don't need, which are part of the code that it uses to determine the next snapTo
offSet
snapToInterval defines the interval for the snapping grid, since we are creating a vertically scrollable list, we define it the same as the screen Height.
The ItemSeparatorComponent is used in FlatList to implement a Horizontal separator line between each item of FlatList. We can customize the separator styling and make it in any color or design.
keyExtractor Used to extract a unique key for a given item at the specified index. Key is used for caching and as the react key to track item re-ordering. The default extractor checks item. key , then item.id , and then falls back to using the index, like React does.
You can use FlatList's ListFooterComponent prop to render some JSX or a component at the end of the list. If you need to know that you are at the end of the list within renderItem , you can check the index in the metadata provided to renderItem against the length of data .
To detect "snap" in FlatList, you can use viewabilityConfig and onViewableItemsChanged Flatlist properties.
See code (for Class and Hooks Components) below where I handle "snap" in onViewChanged method:
import React, {useRef, useState} from 'react';
import {Dimensions, FlatList, View} from 'react-native';
// CLASS COMPONENT API
export class DemoViewabilityConfigClass extends React.Component {
flatlistRef;
constructor(props) {
super(props);
this.state = {
activeIndex: 0,
};
}
onViewChanged = ({viewableItems, index}) => {
console.log('I snapped to', viewableItems);
if (viewableItems.length > 0) {
const {item: activeItem, index: activeIndex} = viewableItems[0];
console.log(activeItem, activeIndex);
this.setState({activeIndex: activeIndex});
}
};
render() {
return (
<FlatList
ref={ref => (this.flatlistRef = ref)}
showsHorizontalScrollIndicator={false}
contentContainerStyle={{
marginHorizontal: 10,
}}
snapToInterval={Dimensions.get('window').width}
decelerationRate={0}
overScrollMode={'never'}
snapToAlignment={'center'}
data={[
{
id: 'something',
other: 'data',
},
{
id: 'else',
other: 'data',
},
]}
onScrollEndDrag={() => console.log('Scroll end')}
horizontal={true}
viewabilityConfig={{itemVisiblePercentThreshold: 70}}
onViewableItemsChanged={this.onViewChanged}
onScrollToIndexFailed={info => {
console.log('Scroll failed', info);
const wait = new Promise(resolve => setTimeout(resolve, 500));
wait.then(() => {
this.flatlistRef.current?.scrollToIndex({
index: info.index,
animated: true,
});
});
}}
renderItem={({item, index}) => {
return (
<View
style={{
marginRight: 20,
height: 20,
width: Dimensions.get('window').width - 20,
backgroundColor: 'red',
}}
/>
);
}}
/>
);
}
}
// HOOKS API
export function DemoViewabilityConfigHooks() {
const flatlistRef = useRef(null);
const [activeEventIndex, setActiveEventIndex] = useState(0);
const onViewChanged = React.useRef(({viewableItems}) => {
console.log('Items ', viewableItems);
if (viewableItems.length > 0) {
const {index, item} = viewableItems[0];
console.log('Active index', index);
setActiveEventIndex(index);
}
});
const viewConfigRef = React.useRef({viewAreaCoveragePercentThreshold: 50});
return (
<FlatList
ref={flatlistRef}
showsHorizontalScrollIndicator={false}
contentContainerStyle={{
marginHorizontal: 10,
}}
snapToInterval={Dimensions.get('window').width}
decelerationRate={0}
overScrollMode={'never'}
snapToAlignment={'center'}
data={[
{
id: 'something',
other: 'data',
},
{
id: 'else',
other: 'data',
},
]}
onScrollEndDrag={() => console.log('Scroll end')}
horizontal={true}
viewabilityConfig={viewConfigRef.current}
onViewableItemsChanged={onViewChanged.current}
onScrollToIndexFailed={info => {
console.log('Scroll failed', info);
const wait = new Promise(resolve => setTimeout(resolve, 500));
wait.then(() => {
flatlistRef.current?.scrollToIndex({
index: info.index,
animated: true,
});
});
}}
renderItem={({item, index}) => {
return (
<View
style={{
marginRight: 20,
height: 20,
width: Dimensions.get('window').width - 20,
backgroundColor: 'red',
}}
/>
);
}}
/>
);
}
If you e.g. need to change component state inside onViewChanged method, you need to wrap these 2 props into Ref() when using Functional components / Hooks API. Otherwise, it results in "Changing onViewableItemsChanged on the fly is not supported." error . Also, you need to pass these props to Flalist by using .current syntax.
Here is GIF example:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With