software-mansion / react-native-reanimated

React Native's Animated library reimplemented
https://docs.swmansion.com/react-native-reanimated/
MIT License
8.83k stars 1.29k forks source link

Android is shaking with useAnimatedScrollHandler #4625

Open aureosouza opened 1 year ago

aureosouza commented 1 year ago

Description

We have a simple Animated FlatList that occupies 30% of the UI initally and lies above a View below. User can drag this list from below all the way up until it occupies 100% of screen. This animation works smothly on iOS, but on Android is quite shaky as user scrolls up slowly.

We tried increasing the input ratio as described here, which helps initially, but then we don't have sync between paddingTop and marginTop anymore.

Reproducible example:

 import React from 'react';
import {Dimensions, StyleSheet, Text, View} from 'react-native';
import Animated, {
  Extrapolation,
  interpolate,
  useAnimatedScrollHandler,
  useAnimatedStyle,
  useSharedValue,
} from 'react-native-reanimated';

const {height: SCREEN_HEIGHT} = Dimensions.get('screen');

const data = Array.from({length: 30}, (_, index) => ({
  id: `${index + 1}`,
  text: `Item ${index + 1}`,
}));

const App = () => {
  const animatedVerticalScroll = useSharedValue(0);

  const listAnimatedStyle = useAnimatedStyle(() => {
    const marginTopValue = interpolate(
      animatedVerticalScroll.value,
      [0, SCREEN_HEIGHT * 0.7],
      [SCREEN_HEIGHT * 0.7, 0],
      Extrapolation.CLAMP,
    );

    const paddingValue = interpolate(
      animatedVerticalScroll.value,
      [0, SCREEN_HEIGHT * 0.7],
      [0, SCREEN_HEIGHT * 0.7],
      Extrapolation.CLAMP,
    );
    return {
      marginTop: marginTopValue,
      paddingTop: paddingValue,
    };
  });

  const footerAnimatedStyle = useAnimatedStyle(() => {
    const height = interpolate(
      animatedVerticalScroll.value,
      [0, SCREEN_HEIGHT * 0.7],
      [0, SCREEN_HEIGHT * 0.7],
      Extrapolation.CLAMP,
    );
    return {
      height: height,
    };
  });

  const scrollHandler = useAnimatedScrollHandler({
    onScroll: event => {
      animatedVerticalScroll.value = event.contentOffset.y;
    },
  });

  const renderItem = ({item}) => (
    <View style={styles.item}>
      <Text>{item.text}</Text>
    </View>
  );

  return (
    <View style={styles.container}>
      <View style={styles.containerBehind}>
        <Text>{'Components behind list'}</Text>
      </View>
      <Animated.FlatList
        bounces={false}
        data={data}
        renderItem={renderItem}
        style={[styles.listContainer, listAnimatedStyle]}
        onScroll={scrollHandler}
        scrollEventThrottle={16}
        ListFooterComponent={<Animated.View style={footerAnimatedStyle} />}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  containerBehind: {
    width: '100%',
    height: '100%',
    flex: 1,
    backgroundColor: 'red',
    alignItems: 'center',
    justifyContent: 'center',
  },
  listContainer: {
    top: 0,
    left: 0,
    position: 'absolute',
    zIndex: 10,
    width: '100%',
    height: '100%',
    backgroundColor: 'green',
  },
  item: {
    height: 50,
  },
});

export default App;

Steps to reproduce

1) Build the minimal reproducible example on Android 2) Try scrolling green list slowly up 3) See shaking/flickering as user is scrolling, iOS is normal.

Snack or a link to a repository

https://github.com/aureosouza/animated-list

Reanimated version

3.3.0

React Native version

0.72.0

Platforms

Android

Workflow

React Native (without Expo)

Acknowledgements

Yes

jvfalco1 commented 1 year ago

I'm facing the same issue only on Android

L0rdCr1s commented 1 year ago

I'm facing same issue, only on Android too, did anyone find a solution for this?

L0rdCr1s commented 1 year ago

@jvfalco1 I think the ratio solution pointed out on the link suggested by @aureosouza works for android, the higher the ratio value the better it gets.

L0rdCr1s commented 1 year ago

@aureosouza I think you have a problem here

[0, SCREEN_HEIGHT * 0.7],
[SCREEN_HEIGHT * 0.7, 0],

I may be wrong but I think that's the reason for missing sync between paddingTop and marginTop values, your input range has 3 values and output range has 2 values.

aureosouza commented 1 year ago

@L0rdCr1s yeah issue seems to be connected with the way Android handles fractional pixel values. We tried doubling the input range, but then we don't have sync between marginTop and paddingTop:

const CUSTOM_HEIGHT = SCREEN_HEIGHT * 0.7;

const listAnimatedStyle = useAnimatedStyle(() => {
   const marginTopValue = interpolate(
     animatedVerticalScroll.value,
     [0, CUSTOM_HEIGHT * 2],
     [CUSTOM_HEIGHT, 0],
     Extrapolation.CLAMP,
   );

   const paddingValue = interpolate(
     animatedVerticalScroll.value,
     [0, CUSTOM_HEIGHT * 2],
     [0, CUSTOM_HEIGHT],
     Extrapolation.CLAMP,
   );
   return {
     marginTop: marginTopValue,
     paddingTop: paddingValue,
   };
 });

 const footerAnimatedStyle = useAnimatedStyle(() => {
   const height = interpolate(
     animatedVerticalScroll.value,
     [0, CUSTOM_HEIGHT * 2],
     [0, CUSTOM_HEIGHT],
     Extrapolation.CLAMP,
   );
   return {
     height: height,
   };
 });

If Android has a different behaviour with the fractional pixel values as described here, could react-native-reanimated adjust the interpolation function to avoid these issues on Android?

One of the explanations I found was that the offset given by iOS is a series like 1, 1.5, 2, 2.5 but android gives offset in decimal values.

aureosouza commented 1 year ago

@piaskowyk are you able to reproduce this scenario on the provided repo example? Any ideas on how to solve this shaking for Android? iOS works well, thanks

efstathiosntonas commented 1 year ago

Is anyone else is experiencing weird scrollY.value behavior in FlashList or FlatList on Android in react-native 0.72.x?

This piece of code worked as a charm on < 0.72 but in 0.72 the scrollY.value won't reach 0 on Android emulator and real device if user is not scrolling on top fast enough.

  const tabsAnimatedStyle = useAnimatedStyle(() => {
    const top = interpolate(
      scrollY.value,
      [0, topMargin + 54],
      [
        Platform.OS === "ios" ? topMargin + 54 : 46,
        Platform.OS === "ios" ? topMargin + 8 : 0
      ],
      Extrapolation.CLAMP
    );
    return {
      top
    };
  });
efstathiosntonas commented 1 year ago

video of the issue from the repo provided on first comment, the flicker is not the only issue, notice that when I try to scroll on top of the list on Android the gesture is stuck and I have to swipe down multiple times to get on top of the list.

https://github.com/software-mansion/react-native-reanimated/assets/717975/290ea93b-b843-43c9-baec-d1bf273cf76a

aureosouza commented 1 year ago

@tjzel any ideas on fix for this case? Android seems to have a specific behaviour when comparing to iOS for interpolation, as pointed as well by @efstathiosntonas

efstathiosntonas commented 1 year ago

@tomekzaw Hi, is there any progress in this issue? It's a blocking for us, we cannot release app update with 0.72 because of this. Thanks

possibly related: https://github.com/software-mansion/react-native-reanimated/issues/4626

tjzel commented 1 year ago

Hi, I'm currently investigating this issue. I will keep you updated as soon as I learn about the causes. I can already give a few answers here:

efstathiosntonas commented 1 year ago

@tjzel any ideas why the list fails to reach the top when user is swiping down? It's happening in the repro but in my case I animate 3 elements, if I make one element static then it works as a charm. It feels like it fails to synchronize all 4 things:

  1. the 3 elements
  2. the list

Can't figure out why it works after making one of the elements static. Please note that my code was working perfectly on rn < 0.72. I've gone through the release notes of rn 0.72.x but I cannot find something that could have caused this. I'm not quite sure tho if going through the commits on release notes page is enough since there might be internal commits that never reach the release changelog 🤷

thanks for looking into this.

tjzel commented 1 year ago

As you may noticed I opened an issue about this on react-native GitHub since I narrowed it down to be stemming only from react-native - after I rewrote your repro to not use react-native-reanimated.

@efstathiosntonas videos you posted seem to be have the same cause as the reported jitter but I'm not sure what are you talking about when you mention those 3 elements. I suggest you try to rewrite your issue to not use react-native-reanimated and see if there is still the same (although maybe a bit more laggy) behavior on both platforms. If yes, then it's most likely not because of reanimated. If not, feel free to open a new issue so we don't have those thing mixed up all in one issue.

efstathiosntonas commented 1 year ago

@tjzel thanks for looking into it. I ll try to create a repro tomorrow to see it in action. What I mean by 3 elements is that the list is inside react-native-tab-view with a row of buttons above and a horizontal list just below the tabs.

  1. ——Buttons———
  2. tab 1 - tab 2 - tab 3
  3. ——horizontal list——-
  4. Flash list

as user is scrolling down the list all elements are moving upwards with a fade out and only the element no.2 stays on top of the screen. If I make that (no.2) element static ie. not moving with the other two then I’m able to scroll on top without “stopping” the list.

efstathiosntonas commented 1 year ago

@tjzel see the attached videos, I know they don't help much, just to have a clear view, please note that this worked as a charm on 0.71.12 reanimated 3.3.0

All elements animated https://github.com/software-mansion/react-native-reanimated/assets/717975/73f289b6-8cc5-4149-9fcd-0d860aafc32f
Tab bar is static, no animations attached at all https://github.com/software-mansion/react-native-reanimated/assets/717975/dec4ff6e-0906-43f0-8ae6-2878d1dc51c1
efstathiosntonas commented 1 year ago

@tjzel I've rolled back to rn 0.71.11 and rean 3.3.0 and I've logged the event of useAnimatedScrollHandler and I've noticed that the eventName starts with a number:

{
  "contentInset"         : {
    "bottom": 0,
    "left"  : 0,
    "right" : 0,
    "top"   : 0
  },
  "contentOffset"        : {
    "x": 0,
    "y": 0
  },
  "contentSize"          : {
    "height": 7487.33349609375,
    "width" : 360
  },
  "eventName"            : "12359onScroll",
  "layoutMeasurement"    : {
    "height": 586.6666870117188,
    "width" : 360
  },
  "responderIgnoreScroll": true,
  "target"               : 12359,
  "velocity"             : {
    "x": 0,
    "y": -9.666666984558105
  }
}

the target: 12359 is prefixing the eventName: 12359onScroll

Did the same with react-native 0.72.3 with rean nightly and the eventName is always onScroll while scrolling the list and the freeze is appearing while scrolling on top fast, on 0.71.11 is smooth as butter.

0.72.3 event:

{
  "contentInset"         : {
    "bottom": 0,
    "left"  : 0,
    "right" : 0,
    "top"   : 0
  },
  "contentOffset"        : {
    "x": 0,
    "y": 966.6666870117188
  },
  "contentSize"          : {
    "height": 10778,
    "width" : 360
  },
  "eventName"            : "onScroll",
  "layoutMeasurement"    : {
    "height": 615,
    "width" : 360
  },
  "responderIgnoreScroll": true,
  "target"               : 7735,
  "velocity"             : {
    "x": 0,
    "y": 0.03999999910593033
  }
}

Please note that I'm using FlashList.

Don't know if this helps or not but it's really weird.

tjzel commented 1 year ago

Sorry for the late reply about that but in regard to event names, yes, it's correct. We are adding a timestamp on the beginning of event name since this PR got merged.

efstathiosntonas commented 1 year ago

@tjzel I’ve managed to fix the animation freeze, I was animating the absolute position top and it turns out it was a matter of number calculations. It’s weird because it was smooth on rn < 0.72.

When I animated the translateY it worked as a charm with the problematic numbers but then I encountered issues with the whole list shifting up leaving space at the bottom.

tldr; the issue must be when animating absolute position values eg. top, interpolating translateY with the same (problematic) numbers/values it works as expected but leads to other ui issues.

tjzel commented 1 year ago

@efstathiosntonas I think it's more about layout and non-layout prop, transform is a non-layout props, meaning, changing component's transform property does not affect other components layout (position), they do not have to adjust. However top is a layout prop, meaning if a component gets its top changed all the other components have to recalculate their position as well.

lsdimagine commented 1 year ago

got the same issue, did you find a solution other than increasing input range? @aureosouza

lsdimagine commented 1 year ago

I found the content offset sent from android scroll view could sometimes go out of order, which causes the shake.

chinamcafee commented 1 year ago

I'm facing the same issue only on Android too, any solution yet?

jsh7195 commented 11 months ago

Shaking appears to occur when the height or coordinate value of the AnimatedFlatList itself changes while scrolling down the AnimatedFlatList

stopgap measure:

header height:200 (position:"absolute")

Flatlist height: 800 (ListHeaderComponent : empty Animated.View height 200)

scroll down

header height: 100, ListHeaderComponent height 100

no shaking

shr-GE commented 11 months ago

i am facing same issue any only on android . any resolutio found yet?

guvenkaranfil commented 10 months ago

Is there any update about this isse @tjzel

tjzel commented 10 months ago

@guvenkaranfil I bumped the issue in React Native's repo but it doesn't seem to have a high priority at the moment. I can try to look into it a bit more and see what's the cause of the problem in React Native but I cannot guarantee how soon would that be.

yshakouri commented 8 months ago

I am facing same issue .

NicholasBoccuzzi commented 7 months ago

Hey @tjzel -- any update on this?

tjzel commented 7 months ago

Unfortunately, no updates. I've been planning to maybe try and fix it in React Native myself but it's sadly pretty far on the priority list - but I haven't forgotten about it!

moddatherrashed commented 5 months ago

did make it work with adjusting the input range, in my case was the input range [100 , 0], and changed it to the following. Note: i am using useAnimatedStyle

height: interpolate(
        scrollY.value,
        [130, 0],
        [0, heightFontRatio],
        Extrapolation.CLAMP,
      )
PatrykMoskal commented 4 months ago

for anyone wondering its bugged with how android deals with floating numbers in input range in interpolation, depending on use case, it should be higher ration than for output range. for my case of 1 to 1 mapping of header height with scroll y offset i had to double the input range for smooth scroll on android

efstathiosntonas commented 3 months ago

@tjzel Hi, is there any chance to take a look into this? Thanks

ps. It's been almost 5 months so I guess it's higher on your list now 😅

tjzel commented 3 months ago

Hi @efstathiosntonas. Yeah it's a bit higher, but now we are in a bumpy period with enabling the New Architecture - I don't want to commit into this, since anything could be subject to change. Once it settles it will be a good time to dive into this.

efstathiosntonas commented 3 months ago

thanks for the update @tjzel! I’ve spent some hours debugging this, 99.9% it’s the scrollview changing it’s height/size when something else decreasing it’s size eg. by decreasing the marginBottom of an element right above the list.

connor-wj-kang commented 2 months ago

for anyone wondering its bugged with how android deals with floating numbers in input range in interpolation, depending on use case, it should be higher ration than for output range. for my case of 1 to 1 mapping of header height with scroll y offset i had to double the input range for smooth scroll on android

double the input range solve the janky animation, it works!!!

jakecurreri commented 1 month ago

for anyone wondering its bugged with how android deals with floating numbers in input range in interpolation, depending on use case, it should be higher ration than for output range. for my case of 1 to 1 mapping of header height with scroll y offset i had to double the input range for smooth scroll on android

This fixed it for me. For Android devices, I doubled my input range, and it resolved the flicker. For example, on iOS, my range is [0, 40]. On Android, my range is now [0, 80].

Must be some under-the-hood rendering for Android devices given smaller input ranges.