How to make a part of ListHeaderComponent sticky on React Native FlatList

5.7k Views Asked by At

I have a React Native FlatList with a ListHeaderComponent with 2 internal Text. The structure is:

  • header
    • section 1 - non sticky
    • section 2 - sticky
  • list items

This means that as the list scrolls up, Section 1 should disappear (non-sticky) whereas section 2 should stay at the top of the list (sticky).

This is the code:

<FlatList
  data={ items }
  renderItem={ renderItem }
  ListHeaderComponent={
     <View>
         <Text>Section 1</Text>
         <Text>Section 2</Text>
     </View>
  }
  stickyHeaderIndices={[0]}
/>

I have to set the indices to [0] so it picks the header but there is no way to select the second within the header. Any ideas?

BTW - I thought of capturing the vertical offset as the list scrolls and then put on the HeaderComponent main <View style={{marginTop: -offset }}> so it simulates a scroll. But my understanding is that Android does not support negative margins.

BTW-2 - I am using react-native-draggable-flatlist so I wouldn't like to put the Text in the list itself as it would complicated the logic of the list items. Thanks!

3

There are 3 best solutions below

1
On

This worked for me when trying to use SectionList

import { View, Animated, FlatList, Text } from 'react-native';

const ITEM_H = 30;
const items = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'];
const renderItem = ({ item }) => <Text style={{ height: ITEM_H }}>{item}</Text>;

export default () => {
  // animated value and interpolate setting
  const offset = React.useRef(new Animated.Value(0)).current;
  const animValue = offset.interpolate({
    inputRange: [0, ITEM_H],
    outputRange: [ITEM_H, 0],
    extrapolate: 'clamp',
  });

  // sticky second item and FlatList
  return (
    <SafeAreaView>
      <View style={{ position: 'relative' }}>
        <Animated.Text
          style={{
            backgroundColor: 'red',
            position: 'absolute',
            top: animValue,
            height: ITEM_H,
            zIndex: 10,
          }}>
          {items[1]}
        </Animated.Text>

        <FlatList
          data={items}
          renderItem={renderItem}
          onScroll={Animated.event([{ nativeEvent: { contentOffset: { y: offset } } }], {
            useNativeDriver: false,
          })}
        />
      </View>
    </SafeAreaView>
  );
};

1
On

How about using Animated API?

  1. You basically set the fixed height for each items.
  2. Only the second item (sticky one) is outside of the FlatList and positioned absolute.
  3. Sticky item has its offset as the item height.
  4. As you scroll up, the offset value will be interpolated into zero, which means sticks at the top.

Working example below.

import { View, Animated, FlatList, Text } from 'react-native';

const ITEM_H = 30;
const items = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'];
const renderItem = ({ item }) => <Text style={{ height: ITEM_H }}>{item}</Text>;

export default () => {
  // animated value and interpolate setting
  const offset = React.useRef(new Animated.Value(0)).current;
  const animValue = offset.interpolate({
    inputRange: [0, ITEM_H],
    outputRange: [ITEM_H, 0],
    extrapolate: 'clamp',
  });

  // sticky second item and FlatList
  return (
    <SafeAreaView>
      <View style={{ position: 'relative' }}>
        <Animated.Text
          style={{
            backgroundColor: 'red',
            position: 'absolute',
            top: animValue,
            height: ITEM_H,
            zIndex: 10,
          }}>
          {items[1]}
        </Animated.Text>

        <FlatList
          data={items}
          renderItem={renderItem}
          onScroll={Animated.event([{ nativeEvent: { contentOffset: { y: offset } } }], {
            useNativeDriver: false,
          })}
        />
      </View>
    </SafeAreaView>
  );
};

1
On

I just found a different way to get this done, not sure if it's best practice though, but couldn't find a different method.

Instead of placing both components inside the header, one sticky and the other not, you can add an item to the front of the array that you'll pass to the data property of the flatlist

const getData = (data: number[]) => {
  const updatedData = ["sticky header", ...data]
  return updatedData;
}
const YourListComponent = () => {
  const someData = [1, 2, 3, 4]
  return 
    <FlatList
      stickyHeaderIndices={[1]} // 0 represents the ListHeaderComponent, 1 represents the first item in the data array
      ListHeaderComponent={
        <View>
          <Text>Header</Text>
        </View>
      }
      data={getData(someData)}
      renderItem={({ item }) => (
        <>
        {typeof item === "string" && (
          <View>
            <Text>{item}</Text>
          </View>
        )}
        {typeof item !== "string" && (
          <View>
            <Text>{item}</Text>
          </View>
        )}
      </>
    )}
  />
}