1

How can I implement a FlatList, in which I have items with dynamic heights and when I scroll to next item it should always snap on top of the screen.

Basically something like

https://www.youtube.com/watch?v=pTtxhuThMew&ab_channel=CatalinMiron in this video but my items will be listed vertically and I wan't to achieve this not once i click to a Pressable but on scroll.

Let's say you have list of images with different heights and each time you scroll, you wan't the next item you scrolling to, snap to top of the screen.

Ozzie
  • 475
  • 1
  • 5
  • 20

2 Answers2

0

I thought this would work:

<FlatList
  data={items}
  renderItem={({ item, index }) => (
    <View style={item}/>
  )}      
  snapToOffsets={items.map(item=>item.height)}
/>

But snapToOffsets are absolute positions, so you need to accumulate the height/width:

// accessor allows to create snap offsets for both width and height
const getSnapPoints = (items, accessor = 'height') => {
  return items.reduce((prev, curr) => {
    const prevPosition = prev[prev.length - 1] || 0;

    prev.push(curr[accessor] + prevPosition);
    return prev;
  }, []);
};

Bringing it all together

import * as React from 'react';
import { Text, View, StyleSheet, FlatList, Dimensions } from 'react-native';
import Constants from 'expo-constants';
import { colorGenerator } from '@phantom-factotum/colorutils';



// generate snap points by accumulating width/height
const getSnapPoints = (items, accessor = 'height') => {
  return items.reduce((prev, curr) => {
    const prevPosition = prev[prev.length - 1] || 0;

    prev.push(curr[accessor] + prevPosition);
    return prev;
  }, []);
};

const isHorizontal = false;

const getRandom = (min = 0, max = 1) => {
  let range = max - min;
  return Math.random() * range + min;
};

// generate random View sizes
const { width, height } = Dimensions.get('screen');
const items = colorGenerator(10).map((color) => {
  return {
    height: Math.round(getRandom(200, height)),
    width: Math.round(getRandom(200, width)),
    backgroundColor: color,
  };
});

export default function App() {
  return (
    <View style={styles.container}>
      <FlatList
        data={items}
        renderItem={({ item, index }) => (
          <View style={item}>
            <Text>
              Item {index + 1} of {items.length}
            </Text>
          </View>
        )}
        // generate snapPoints base on if isHorizontal
        snapToOffsets={getSnapPoints(items, isHorizontal ? 'width' : 'height')}
        horizontal={isHorizontal}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    paddingTop: Constants.statusBarHeight,
    backgroundColor: '#ecf0f1',
    padding: 8,
  },
  paragraph: {
    margin: 24,
    fontSize: 18,
    fontWeight: 'bold',
    textAlign: 'center',
  },
});
PhantomSpooks
  • 2,877
  • 2
  • 8
  • 13
  • The snapToOffsets doesnt work for web, so try testing it on an emulator – PhantomSpooks Dec 15 '22 at 22:15
  • The first example I started to make but couldnt figure out why it wouldnt work (forgive the mess I was trying stuff) https://snack.expo.dev/LTk_YGB28?platform=android – PhantomSpooks Dec 15 '22 at 22:27
  • Thank you very much, that was very helpful to me. But what if i am not listing directly images like in your example ? i mean if i have some header and footer also ? Just gave it a try, and i get LOG [NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN] in a flexbox layout – Ozzie Dec 16 '22 at 13:02
  • You could add the header and footer to items e.g `const withHeaderAndFooter =[].concat({height:headerHeight},items,{height:footerHeight})` and then `getSnapPoints(withHeaderAndFooter)` – PhantomSpooks Dec 16 '22 at 18:16
0
      onViewableItemsChanged={info => {
        if (info.changed.length > 0 && info.changed[info.changed.length - 1].isViewable) {
          setIndex(info.changed[info.changed.length - 1].index);
        }
      }}
      viewabilityConfig={{
        viewAreaCoveragePercentThreshold: 10,
        waitForInteraction: true,
      }}
      onScrollEndDrag={() => {
        listRef.current.scrollToIndex({
          index: index,
          animated: true,
          viewOffset: insets.top,
        });
      }}

meanwhile found a quick solution with following props

Ozzie
  • 475
  • 1
  • 5
  • 20