Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to detect which item will be snapped to, when swiping in FlatList that snaps to interval?

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

like image 696
Esben von Buchwald Avatar asked Aug 23 '19 22:08

Esben von Buchwald


People also ask

What is snapToInterval?

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.

What is item separator in FlatList?

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.

What is the use of keyExtractor in FlatList?

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.

How do I find the last item in FlatList?

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 .


1 Answers

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:

enter image description here

like image 67
Stefan Majiros Avatar answered Oct 18 '22 03:10

Stefan Majiros