Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Android] measure giving incorrect values #3188

Closed
1 of 3 tasks
abedolinger opened this issue Apr 20, 2022 · 9 comments · Fixed by #3414
Closed
1 of 3 tasks

[Android] measure giving incorrect values #3188

abedolinger opened this issue Apr 20, 2022 · 9 comments · Fixed by #3414
Assignees
Labels
Bug Platform: Android This issue is specific to Android 🏠 Reanimated 2 Repro provided A reproduction with a snippet of code, snack or repo is provided Reproducible 🎉

Comments

@abedolinger
Copy link

abedolinger commented Apr 20, 2022

Description

I'm using measure with an animated ref. On iOS, this works as expected and outputs the right values. On Android, the values are (as far as I can tell) unrelated to the component's size, and seem to change over time.

Expected behavior

I'm animating some text using ReText, and using the final size of that text in a padding animation. So, at the end of the ReText change, I measure the view that's around the text, and set some padding based on that. The measure step is where the below console logs come from.

Steps to Reproduce includes a minimal example with more context.

Output of console.log(measured) on iOS:

{
  "height": 17,
  "pageX": 174,
  "pageY": 296.3333231508732,
  "width": 44.66667175292969,
  "x": 0,
  "y": -0.3333333432674408
}

This is great and gives me exactly what I need.

Actual behavior

Output of console.log(measured) on Android:

{
  "height": 6.740754805355325e-33,
  "pageX": -8.02794075012207,
  "pageY": -1.1921103748591122e-7,
  "width": 9.219562986332269e-41,
  "x": 7.715996607199058e+31,
  "y": 5.14818588087719e+22
}

These values aren't stable. For example if I run this again directly afterward (this is after the ReText has stopped resizing), I get:

{
  "height": -8.048060130728983e+34,
  "pageX": -1.6282598256782247e+32,
  "pageY": 1.7796490496925177e-43,
  "width": 9.219562986332269e-41,
  "x": 7.715996607199058e+31,
  "y": 5.14818588087719e+22
},

These values are less helpful.

Snack or minimal code example

Simplified version of my implementation below.

  const textContainer = useAnimatedRef();
  const textValue = useAnimatedValue(0);
  const textPadding = useAnimatedValue(0);
  ...

  const animatedText = useDerivedValue(() => textValue.value.toFixed(2));

  const containerStyle = useAnimatedStyle(() => ({
    paddingLeft: textPadding.value,
  }));

  useEffect(() => {
    textValue = withTiming(100, { duration: 2000 }, () => {
      'worklet';

      const measured = measure(text);
      console.log(measured);
      textPadding.value = withTiming(measured.width)    
    });
  }, []);

  return (
    <Animated.View ref={textContainer} style={containerStyle}>
      <ReText text={animatedText} />
    </Animated.View>
  );

Package versions

name version
react-native 64.2
react-native-reanimated 2.2.4
NodeJS 16.13.0
Xcode 13.3.1
Java openjdk 11.0.11
Gradle Gradle 6.9

Affected platforms

  • Android
  • iOS
  • Web
@abedolinger abedolinger added the Needs review Issue is ready to be reviewed by a maintainer label Apr 20, 2022
@github-actions github-actions bot added Platform: Android This issue is specific to Android Repro provided A reproduction with a snippet of code, snack or repo is provided labels Apr 20, 2022
@pugson
Copy link

pugson commented May 19, 2022

Running into the same issue. Have you managed to patch this up somehow or did you end up having to use another method for grabbing width?

@abedolinger
Copy link
Author

Unfortunately I did not find a good way around this, and we ended up changing the design.

@boxedition
Copy link

boxedition commented Jun 29, 2022

##Update
For some unknown reason if i change from <View ref={aRef}> to <View ref={aRef} style={style}>, it gives the proper number...

##Original
Having the same kind of problem. While logging it gave random values
https://snack.expo.dev/@boxedition/ripple-measure-random-values

Packages Info

Name Version
expo 45
node 16.14.2
react-native-reanimated 2.8.0
react-native-gesture-handler 2.2.1
react 17.0.2

Logs on my end:
Object {
"height": 7.673845534663173e+22,
"pageX": -0.00003062416362809017,
"pageY": -5.308031791884105e-12,
"width": 9.183689745645554e-41,
"x": 7.715996607199058e+31,
"y": 5.14818588087719e+22,
}
Object {
"height": -3.679299364635867e-21,
"pageX": -0.00003062416362809017,
"pageY": -5.308031791884105e-12,
"width": 4.627788178432708e-41,
"x": 7.715996607199058e+31,
"y": 5.14818588087719e+22,
}
Object {
"height": -4.1168678491221554e-30,
"pageX": -0.00003062416362809017,
"pageY": -5.308031791884105e-12,
"width": 9.219562986332269e-41,
"x": 7.715996607199058e+31,
"y": 5.14818588087719e+22,
}

@tomekzaw
Copy link
Member

Hello @256hz! Thanks for reporting the problem. Unfortunately, I wasn't able to reproduce this issue on react-native-reanimated@2.9.1 and react-native@0.69.1.

Also, I've needed to modify a few lines in the provided code snippets, e.g. change useAnimatedValue to useSharedValue, or replace measure(text) with measure(textContainer). Here's the final version of the code:

App.tsx
import React, { useEffect } from 'react';
import Animated, {
  measure,
  useAnimatedRef,
  useAnimatedStyle,
  useDerivedValue,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';
import { ReText } from 'react-native-redash';

export default function App() {
  const textContainer = useAnimatedRef<Animated.View>();
  const textValue = useSharedValue(0);
  const textPadding = useSharedValue(0);

  const animatedText = useDerivedValue(() => textValue.value.toFixed(2));

  const containerStyle = useAnimatedStyle(() => ({
    paddingLeft: textPadding.value,
  }));

  useEffect(() => {
    textValue.value = withTiming(100, { duration: 2000 }, () => {
      'worklet';

      const measured = measure(textContainer);
      console.log(measured);
      textPadding.value = withTiming(measured.width);
    });
  }, []);

  return (
    <Animated.View ref={textContainer} style={containerStyle}>
      <ReText text={animatedText} />
    </Animated.View>
  );
}

Here are the logs from iOS simulator:

{"height": 17, "pageX": 0, "pageY": 0, "width": 390, "x": 0, "y": 0}
{"height": 17, "pageX": 0, "pageY": 0, "width": 390, "x": 0, "y": 0}
{"height": 17, "pageX": 0, "pageY": 0, "width": 390, "x": 0, "y": 0}

And here are the logs from Android emulator:

{"height": 48.3636360168457, "pageX": 0, "pageY": 0, "width": 392.7272644042969, "x": 0, "y": 0}
{"height": 48.3636360168457, "pageX": 0, "pageY": 0, "width": 392.7272644042969, "x": 0, "y": 0}
{"height": 48.3636360168457, "pageX": 0, "pageY": 0, "width": 392.7272644042969, "x": 0, "y": 0}

🐛 Reproduction

The good news is that I was able to successfully reproduce the issue on the example provided by @boxedition.

Here are the logs from Android emulator:

{"height": -13.5, "pageX": 1.0375870230039667e-25, "pageY": -1.1921090958821878e-7, "width": 9.219562986332269e-41, "x": 7.715996607199058e+31, "y": 5.14818588087719e+22}
{"height": 2.1061754513598618e+24, "pageX": -2.0069851875305176, "pageY": 1.5834672646870433e-43, "width": 9.183689745645554e-41, "x": 7.715996607199058e+31, "y": 5.14818588087719e+22}
{"height": 0, "pageX": 4.105524240778849e-41, "pageY": 0, "width": 0, "x": 7.715996607199058e+31, "y": 5.14818588087719e+22}
{"height": 4.555671727651343e-29, "pageX": -564916108918784, "pageY": 1.7796490496925177e-43, "width": 9.219562986332269e-41, "x": 7.715996607199058e+31, "y": 5.14818588087719e+22}
{"height": 7.806017173429253e-39, "pageX": -564916108918784, "pageY": 1.7796490496925177e-43, "width": 9.219562986332269e-41, "x": 7.715996607199058e+31, "y": 5.14818588087719e+22}

❓ Explanation

In the old architecture (Paper), React Native on Android uses an optimization technique called view flattening which skips unnecessary views which don't affect the app visually when mounting the component tree. Most likely these views have been flattened so they don't exist in the native tree hierarchy.

In such case, mUIManager.resolveView(viewTag) throws the following IllegalViewOperationException:

2022-07-27 15:34:15.530 25658-25658/com.swmansion.reanimated.example W/System.err: com.facebook.react.uimanager.IllegalViewOperationException: Trying to resolve view with tag 37 which doesn't exist
2022-07-27 15:34:15.531 25658-25658/com.swmansion.reanimated.example W/System.err:     at com.facebook.react.uimanager.NativeViewHierarchyManager.resolveView(NativeViewHierarchyManager.java:102)
2022-07-27 15:34:15.531 25658-25658/com.swmansion.reanimated.example W/System.err:     at com.facebook.react.uimanager.UIManagerModule.resolveView(UIManagerModule.java:950)
2022-07-27 15:34:15.531 25658-25658/com.swmansion.reanimated.example W/System.err:     at com.swmansion.reanimated.NodesManager.measure(NodesManager.java:56)
2022-07-27 15:34:15.531 25658-25658/com.swmansion.reanimated.example W/System.err:     at com.swmansion.reanimated.NativeProxy.measure(NativeProxy.java:232)
...

However, in NodesManager.java we catch this type of exception and for unknown reasons return new float[] {} which is an empty array of float values with zero length:

public float[] measure(int viewTag) {
View view;
try {
view = mUIManager.resolveView(viewTag);
} catch (IllegalViewOperationException e) {
e.printStackTrace();
return (new float[] {});
}
return NativeMethodsHelper.measure(view);
}

Next, in NativeProxy.cpp we get 6 first elements of the array which should represent x, y, pageX, pageY, width and height, respectively:

std::vector<std::pair<std::string, double>> NativeProxy::measure(int viewTag) {
auto method =
javaPart_->getClass()->getMethod<local_ref<JArrayFloat>(int)>("measure");
local_ref<JArrayFloat> output = method(javaPart_.get(), viewTag);
size_t size = output->size();
auto elements = output->getRegion(0, size);
std::vector<std::pair<std::string, double>> result;
result.push_back({"x", elements[0]});
result.push_back({"y", elements[1]});
result.push_back({"pageX", elements[2]});
result.push_back({"pageY", elements[3]});
result.push_back({"width", elements[4]});
result.push_back({"height", elements[5]});
return result;
}

Since the array has length of zero, we get non-deterministic values when reading its elements.

Additionally, adding style property disables view flattening, so that's why it helps in this case.

From the perspective of Reanimated, a proper solution would be to throw an error and propagate it to JS or return null from measure in such case.

✅ Solution

However, after changing <View ref={aRef}> to <Animated.View ref={aRef}> in Ripple.tsx:77 I get correct and repeatable results on both platforms:

Ripple.tsx
import React from 'react';
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
import { TapGestureHandler, TapGestureHandlerGestureEvent } from 'react-native-gesture-handler';
import Animated, {
    measure,
    runOnJS,
    useAnimatedGestureHandler,
    useAnimatedRef,
    useAnimatedStyle,
    useSharedValue,
    withTiming
} from 'react-native-reanimated';

interface IRippleProps {
    style?: StyleProp<ViewStyle>,
    onTap?: () => void,
}

const Ripple: React.FC<IRippleProps> = ({ style, onTap, children }) => {
    const aRef = useAnimatedRef<Animated.View>()
    const centerX = useSharedValue(0)
    const centerY = useSharedValue(0)
    const scale = useSharedValue(0)

    //const width = useSharedValue(0)
    //const height = useSharedValue(0)

    const tapGestureEvent = useAnimatedGestureHandler<TapGestureHandlerGestureEvent>({
        onStart(event, context) {
            const layout = measure(aRef)
            console.log(layout)
            
            //width.value = layout.width
            //height.value = layout.height
            
            centerX.value = event.x
            centerY.value = event.y
            scale.value = 0

            scale.value = withTiming(1, { duration: 450 })
        },
        onActive(event, context) {
            if (onTap) {
                runOnJS(onTap)()
            }
        },
        onEnd(event, context) {

        },
    })

    const rStyle = useAnimatedStyle(() => {
        //const circleRadius = Math.sqrt(width.value ** 2 + height.value ** 2)
        const circleRadius = 500

        const translateX = centerX.value - circleRadius
        const translateY = centerY.value - circleRadius

        return {
            position: 'absolute',
            backgroundColor: 'red',
            width: circleRadius * 2,
            height: circleRadius * 2,
            borderRadius: circleRadius,
            top: 0,
            left: 0,
            transform: [
                { translateX },
                { translateY },
                { scale: scale.value },
            ],
            opacity: 0.2,
        }
    })

    return (
        <Animated.View ref={aRef}>
            <TapGestureHandler onGestureEvent={tapGestureEvent}>
                <Animated.View style={[style, { overflow: 'hidden' }]}>
                    <View style={style}>{children}</View>
                    <Animated.View style={[rStyle]} />
                </Animated.View>
            </TapGestureHandler>
        </Animated.View>
    )
};

export default Ripple;

Also, it works fine when I change <View ref={aRef}> into <View ref={aRef} collapsable={false}>.

Here are the logs from iOS:

{"height": 250, "pageX": 70, "pageY": 297, "width": 250, "x": 70, "y": 297}
{"height": 250, "pageX": 70, "pageY": 297, "width": 250, "x": 70, "y": 297}
{"height": 250, "pageX": 70, "pageY": 297, "width": 250, "x": 70, "y": 297}

Here are the logs from Android:

{"height": 250.18182373046875, "pageX": 71.2727279663086, "pageY": 258.5454406738281, "width": 250.18182373046875, "x": 0, "y": 0}
{"height": 250.18182373046875, "pageX": 71.2727279663086, "pageY": 258.5454406738281, "width": 250.18182373046875, "x": 0, "y": 0}
{"height": 250.18182373046875, "pageX": 71.2727279663086, "pageY": 258.5454406738281, "width": 250.18182373046875, "x": 0, "y": 0}

💡 Conclusion

If you want to use measure via useAnimatedRef, you need to set collapsable={false} property on View in order to disable view flattening on Android.

@Nadimkhan120
Copy link

@tomekzaw setting collapsable={false} does not fix the issue on android. i am always getting 0 for value of x

@longb1997
Copy link

longb1997 commented Aug 6, 2024

@tomekzaw setting collapsable={false} does not fix the issue on android. i am always getting 0 for value of x

i face the same issue, always 0 for x and y
IOS still work like a charm

@WayneKim92
Copy link

WayneKim92 commented Aug 22, 2024

I face the same issue.

If you are using the ref's current?.measure method, it is recommended to use onLayout instead. But if you use pageX and pageY, there seems to be no way yet.

@tomekzaw
Copy link
Member

tomekzaw commented Sep 2, 2024

i face the same issue, always 0 for x and y
IOS still work like a charm

@longb1997 Looks like this might have been fixed with #6413.

@Lakston
Copy link

Lakston commented Jan 14, 2025

was this released ?

Ok I found the release (3.16.0) but I still have the case of measure giving values (height and width are 0 and pageX is wrong value)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug Platform: Android This issue is specific to Android 🏠 Reanimated 2 Repro provided A reproduction with a snippet of code, snack or repo is provided Reproducible 🎉
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants