Skip to content

Last drawn item remains the viewport while scrolling #278

@vargajacint

Description

@vargajacint

Summary

It's hard to describe the bug (so I encourage you to check the attached video), since it's prominent for animated lists.

It seems the last drawn item always stays at the top, and only gets removed when the new card is fully in the viewport.

My first thought was some zIndex issue, but increasing the zIndex for every upcoming item did not help.

When I increased the drawDistance, the issue gets pushed further (so I think it's tied to the drawn content calculation). Maybe the last drawn item is duplicated? idk...

When I tested it with FlatList, it worked as expected... When I turned off the recycling, the issue still remains. Btw, the issue was the same with FlashList, so I'm certain it's something related to the recycling view concept.

Reproduction: https://snack.expo.dev/@jacint_fair/0fb4a5

Media

FlatList LegendList
flatlist.1.mp4
legendlist.2.mp4

Reproduction, in case of expo snak is broken

import { useCallback, useMemo } from 'react';
import { Dimensions, SafeAreaView, StyleSheet, Text, View } from 'react-native';

import { AnimatedLegendList } from '@legendapp/list/reanimated';
import Animated, {
  useSharedValue,
  useAnimatedScrollHandler,
  useAnimatedStyle,
  interpolate,
  Extrapolation,
} from 'react-native-reanimated';

const BACKGROUND_COLORS = ['orange', 'green', 'pink'];
const DATA = [...new Array(30)].map((_, i) => i);
const ITEM_HEIGHT = Dimensions.get('window').height;
const SEPARATOR_HEIGHT = 20;
const TOTAL_HEIGHT = ITEM_HEIGHT + SEPARATOR_HEIGHT;

const keyExtractor = (_, index) => index;

const ItemSeparatorComponent = () => {
  return <View style={{ height: SEPARATOR_HEIGHT }} />;
};

export default function App() {
  const scrollY = useSharedValue(0);

  const renderItem = useCallback(
    ({ index }) => {
      return <Item index={index} scrollY={scrollY} />;
    },
    [scrollY]
  );

  const scrollHandler = useAnimatedScrollHandler({
    onScroll(event) {
      scrollY.set(event.contentOffset.y / TOTAL_HEIGHT);
    },
  });

  return (
    <SafeAreaView style={styles.container}>
      <AnimatedLegendList
        data={DATA}
        onScroll={scrollHandler}
        renderItem={renderItem}
        keyExtractor={keyExtractor}
        ItemSeparatorComponent={ItemSeparatorComponent}
        contentContainerStyle={{ flexGrow: 1 }}
        estimatedItemSize={ITEM_HEIGHT}
        snapToInterval={TOTAL_HEIGHT}
        snapToAlignment="start"
        decelerationRate="fast"
        disableIntervalMomentum
      />
    </SafeAreaView>
  );
}

function Item({ index, scrollY }) {
  const animatedStyle = useAnimatedStyle(() => {
    return {
      transform: [
        {
          scale: interpolate(
            scrollY.value,
            [index - 1, index, index + 1],
            [1, 1, 0.9],
            Extrapolation.CLAMP
          ),
        },

        {
          translateY: interpolate(
            scrollY.value,
            [index - 1, index - 0.5, index, index + 1],
            [0, 0, 0, TOTAL_HEIGHT],
            Extrapolation.CLAMP
          ),
        },
      ],
    };
  }, [scrollY, index]);

  const backgroundColor = useMemo(() => {
   return BACKGROUND_COLORS[Math.floor(Math.random() * BACKGROUND_COLORS.length)];
  }, []);

  return (
    <Animated.View style={[styles.item, { backgroundColor }, animatedStyle]}>
      <Text>{index}</Text>
    </Animated.View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    backgroundColor: 'red',
    padding: 8,
  },
  item: {
    height: ITEM_HEIGHT,
    width: 300,
  },
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions