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

1

7

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

Featherstitch answered 23/8, 2019 at 22:56 Comment(0)
H
6

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

Hearsay answered 3/10, 2021 at 17:3 Comment(1)
And, just btw, they improved performance a lot in "react-native-snap-carousel": "4.0.0-beta.6" - but I am not sure if it's already merged in master or not).Hearsay

© 2022 - 2024 — McMap. All rights reserved.