react-native-reanimated icon indicating copy to clipboard operation
react-native-reanimated copied to clipboard

Android is shaking with useAnimatedScrollHandler

Open aureosouza opened this issue 1 year ago • 36 comments

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

aureosouza avatar Jun 26 '23 12:06 aureosouza

I'm facing the same issue only on Android

jvfalco1 avatar Jun 26 '23 13:06 jvfalco1

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

L0rdCr1s avatar Jun 27 '23 12:06 L0rdCr1s

@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 avatar Jun 28 '23 05:06 L0rdCr1s

@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.

L0rdCr1s avatar Jun 28 '23 05:06 L0rdCr1s

@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 avatar Jun 28 '23 09:06 aureosouza

@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

aureosouza avatar Jul 05 '23 08:07 aureosouza

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 avatar Jul 08 '23 13:07 efstathiosntonas

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

efstathiosntonas avatar Jul 10 '23 06:07 efstathiosntonas

@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

aureosouza avatar Jul 10 '23 07:07 aureosouza

@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

efstathiosntonas avatar Jul 12 '23 13:07 efstathiosntonas

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:

  • scrollEventThrottle property is only available on iOS (but it's still kind-of irrelevant here - Android tries to send the event once per frame)
  • interpolate function is not the issue here. First of all, it's use here is a bit of an overkill (but is still valid). When I swapped it everywhere to a simple clamped linear function e.g.:
-    const height = interpolate(
-      animatedVerticalScroll.value,
-      [0, SCREEN_HEIGHT * 0.7],
-      [0, SCREEN_HEIGHT * 0.7],
-      Extrapolation.CLAMP
-    );
+    const height =
+      animatedVerticalScroll.value > SCREEN_HEIGHT * 0.7
+        ? SCREEN_HEIGHT * 0.7
+        : animatedVerticalScroll.value;

The issue persists.

  • After trying different options I narrowed down that it's marginTop property that is causing weird flickering - if you pass sharedValue to it in this setup. Now I'm trying to figure out why.

tjzel avatar Jul 13 '23 12:07 tjzel

@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.

efstathiosntonas avatar Jul 13 '23 12:07 efstathiosntonas

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.

tjzel avatar Jul 17 '23 16:07 tjzel

@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 avatar Jul 17 '23 17:07 efstathiosntonas

@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 avatar Jul 17 '23 17:07 efstathiosntonas

@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.

efstathiosntonas avatar Jul 19 '23 19:07 efstathiosntonas

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.

tjzel avatar Aug 04 '23 14:08 tjzel

@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.

efstathiosntonas avatar Aug 04 '23 18:08 efstathiosntonas

@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.

tjzel avatar Aug 18 '23 11:08 tjzel

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

lsdimagine avatar Sep 01 '23 04:09 lsdimagine

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

lsdimagine avatar Sep 01 '23 06:09 lsdimagine

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

chinamcafee avatar Sep 10 '23 12:09 chinamcafee

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

jsh7195 avatar Sep 24 '23 09:09 jsh7195

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

shr-GE avatar Sep 27 '23 14:09 shr-GE

Is there any update about this isse @tjzel

guvenkaranfil avatar Oct 31 '23 19:10 guvenkaranfil

@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.

tjzel avatar Nov 02 '23 19:11 tjzel

I am facing same issue .

yshakouri avatar Dec 19 '23 14:12 yshakouri

Hey @tjzel -- any update on this?

NicholasBoccuzzi avatar Jan 23 '24 23:01 NicholasBoccuzzi

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!

tjzel avatar Jan 26 '24 15:01 tjzel

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,
      )

moddatherrashed avatar Mar 27 '24 16:03 moddatherrashed