Skip to content

Commit

Permalink
Implement missing measurements on android (#6413)
Browse files Browse the repository at this point in the history
## Summary
Implement measurement of relative coordinates on Android.

## Test plan
I've added more logic to runtime tests to include parent component
changes in calculations.


<details><summary> Example code to log measurement </summary>

```tsx
import React from 'react';
import { Button, StyleSheet, View } from 'react-native';
import type { MeasuredDimensions } from 'react-native-reanimated';
import Animated, {
  measure,
  runOnUI,
  useAnimatedRef,
  useSharedValue,
} from 'react-native-reanimated';

const EXPECTED_MEASUREMENT = {
  height: 100,
  pageX: 302,
  width: 80,
  x: 120,
  y: 150,
};

export default function App() {
  const animatedRef = useAnimatedRef<Animated.View>();

  const measurement = useSharedValue<MeasuredDimensions | null>(null);

  const handlePress = () => {
    runOnUI(() => {
      'worklet';
      measurement.value = measure(animatedRef);
      console.log(measurement.value);
    })();
  };

  return (
    <View style={styles.container}>
      <View style={styles.bigBox}>
        <Animated.View ref={animatedRef} style={styles.box} />
      </View>
      <Button onPress={handlePress} title="Click me" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  bigBox: {
    position: 'absolute',
    top: 0,
    left: EXPECTED_MEASUREMENT.pageX - EXPECTED_MEASUREMENT.x,
    height: 300,
    width: 300,
    borderColor: '#b58df1',
    borderWidth: 2,
  },
  box: {
    top: EXPECTED_MEASUREMENT.y,
    left: EXPECTED_MEASUREMENT.x,
    height: EXPECTED_MEASUREMENT.height,
    width: EXPECTED_MEASUREMENT.width,
    backgroundColor: '#b58df1',
  },
});

```
</summary>
  • Loading branch information
Latropos authored and tjzel committed Aug 28, 2024
1 parent eebc3d2 commit ad3145b
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export default function RuntimeTestsExample() {
testSuiteName: 'advanced API',
importTest: () => {
require('./tests/advancedAPI/useFrameCallback.test');
// require('./tests/advancedAPI/measure.test'); // crash on Android
require('./tests/advancedAPI/measure.test');
},
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,31 @@ import Animated, {
import { describe, expect, test, render, wait, registerValue, getRegisteredValue } from '../../ReJest/RuntimeTestsApi';
import { ComparisonMode } from '../../ReJest/types';

describe('Test measuring component before nad after animation', () => {
const DEFAULT_STYLE = {
width: 100,
height: 50,
margin: 0,
top: 0,
left: 0,
};
const DEFAULT_PARENT_MARGIN = 50;

type TestCase = {
initialStyle: AnimatableValueObject;
finalStyle: AnimatableValueObject;
initialParentMargin: number;
finalParentMargin: number;
};

describe('Test measuring component before nad after animation of the component and its parent', () => {
const INITIAL_MEASURE = 'INITIAL_MEASURE';
const FINAL_MEASURE = 'FINAL_MEASURE';
const MeasuredComponent = ({
initialStyle,
finalStyle,
}: {
initialStyle: AnimatableValueObject;
finalStyle: AnimatableValueObject;
}) => {
const MeasuredComponent = ({ initialStyle, finalStyle, initialParentMargin, finalParentMargin }: TestCase) => {
const measuredInitial = useSharedValue<MeasuredDimensions | null>(null);
const measuredFinal = useSharedValue<MeasuredDimensions | null>(null);

const styleSV = useSharedValue(initialStyle);
const parentMarginSV = useSharedValue(initialParentMargin);

registerValue(INITIAL_MEASURE, measuredInitial);
registerValue(FINAL_MEASURE, measuredFinal);
Expand All @@ -39,87 +50,134 @@ describe('Test measuring component before nad after animation', () => {
return styleSV.value;
});

const parentMargin = useAnimatedStyle(() => {
return { margin: parentMarginSV.value };
});

useEffect(() => {
runOnUI(() => {
measuredInitial.value = measure(ref);
})();
setTimeout(() => {
runOnUI(() => {
measuredInitial.value = measure(ref);
})();
}, 50);
});

useEffect(() => {
styleSV.value = withDelay(
50,
400,
withTiming(finalStyle, { duration: 300 }, () => {
measuredFinal.value = measure(ref);
}),
);
parentMarginSV.value = withDelay(
400,
withTiming(finalParentMargin, { duration: 200 }, () => {}),
);
});

return (
<View style={styles.container}>
<Animated.View style={[styles.container, parentMargin]}>
<Animated.View ref={ref} style={[styles.smallBox, animatedStyle]} />
</View>
</Animated.View>
);
};

test.each([
[{ width: 40 }, { width: 100 }],
[{ height: 40 }, { height: 100 }],
[
{ height: 80, width: 20 },
{ height: 25, width: 85 },
],
[{ margin: 40 }, { margin: 60 }],
[
{ height: 40, width: 40, margin: 40 },
{ height: 100, width: 100, margin: 60 },
],
[
{ height: 40, width: 40, margin: 40, top: 0 },
{ height: 100, width: 100, margin: 60, top: 40 },
],
[
{ height: 40, width: 40, margin: 40, left: 0 },
{ height: 100, width: 100, margin: 60, left: 40 },
],
[
{ height: 40, width: 40, margin: 40, left: 0, top: 50 },
{ height: 100, width: 100, margin: 60, left: 40, top: 0 },
],
])('Measure component animating from ${0} to ${1}', async ([initialStyle, finalStyle]) => {
await render(<MeasuredComponent initialStyle={initialStyle} finalStyle={finalStyle} />);
await wait(450);
const measuredInitial = (await getRegisteredValue(INITIAL_MEASURE)).onJS as unknown as MeasuredDimensions;
const measuredFinal = (await getRegisteredValue(FINAL_MEASURE)).onJS as unknown as MeasuredDimensions;

// Check the distance from the top
const finalStyleFull = { width: 100, height: 100, margin: 0, top: 0, left: 0, ...finalStyle };
const initialStyleFull = { width: 100, height: 100, margin: 0, top: 0, left: 0, ...initialStyle };

if ('height' in finalStyle && 'height' in initialStyle) {
expect(measuredFinal.height).toBeWithinRange(finalStyle.height - 2, finalStyle.height + 2);
expect(measuredInitial.height).toBeWithinRange(initialStyle.height - 2, initialStyle.height + 2);
}

if ('width' in finalStyle && 'width' in initialStyle) {
expect(measuredFinal.width).toBeWithinRange(finalStyle.width - 2, finalStyle.width + 2);
expect(measuredInitial.width).toBeWithinRange(initialStyle.width - 2, initialStyle.width + 2);
}

// Absolute translation equals relative translation
expect(measuredFinal.pageX - measuredInitial.pageX).toBe(measuredFinal.x - measuredInitial.x);
expect(measuredFinal.pageY - measuredInitial.pageY).toBe(measuredFinal.y - measuredInitial.y);

// Check distance from top and from left
const expectedInitialDistanceFromTop = initialStyleFull.margin + initialStyleFull.top;
expect(measuredInitial.y).toBeWithinRange(expectedInitialDistanceFromTop - 2, expectedInitialDistanceFromTop + 2);
const expectedFinalDistanceFromTop = finalStyleFull.margin + finalStyleFull.top;
expect(measuredFinal.y).toBeWithinRange(expectedFinalDistanceFromTop - 2, expectedFinalDistanceFromTop + 2);

const expectedInitialDistanceFromLeft = initialStyleFull.margin + initialStyleFull.left;
expect(measuredInitial.x).toBeWithinRange(expectedInitialDistanceFromLeft - 2, expectedInitialDistanceFromLeft + 2);
const expectedFinalDistanceFromLeft = finalStyleFull.margin + finalStyleFull.left;
expect(measuredFinal.x).toBeWithinRange(expectedFinalDistanceFromLeft - 2, expectedFinalDistanceFromLeft + 2);
});
{ initialStyle: { width: 40 }, finalStyle: { width: 100 }, initialParentMargin: 30, finalParentMargin: 60 },
{ initialStyle: { height: 40 }, finalStyle: { height: 100 }, initialParentMargin: 30, finalParentMargin: 60 },
{ initialStyle: { margin: 40 }, finalStyle: { margin: 60 }, initialParentMargin: 30, finalParentMargin: 60 },
{
initialStyle: { height: 80, width: 20 },
finalStyle: { height: 25, width: 85 },
initialParentMargin: 30,
finalParentMargin: 60,
},
{
initialStyle: { height: 40, width: 40, margin: 40 },
finalStyle: { height: 100, width: 100, margin: 60 },
initialParentMargin: 0,
finalParentMargin: 100,
},
{
initialStyle: { height: 40, width: 40, margin: 40, top: 0 },
finalStyle: { height: 100, width: 100, margin: 60, top: 40 },
initialParentMargin: 30,
finalParentMargin: 30,
},
{
initialStyle: { height: 40, width: 40, margin: 40, left: 0 },
finalStyle: { height: 100, width: 100, margin: 60, left: 40 },
initialParentMargin: 0,
finalParentMargin: 0,
},
{
initialStyle: { height: 40, width: 40, margin: 40, left: 0, top: 50 },
finalStyle: { height: 100, width: 100, margin: 60, left: 40, top: 0 },
initialParentMargin: 100,
finalParentMargin: 60,
},
] as Array<TestCase>)(
'Measure test *****%#*****',
async ({ initialStyle, finalStyle, initialParentMargin, finalParentMargin }) => {
await render(
<MeasuredComponent
initialStyle={initialStyle}
finalStyle={finalStyle}
initialParentMargin={initialParentMargin}
finalParentMargin={finalParentMargin}
/>,
);

await wait(1000);

const measuredInitial = (await getRegisteredValue(INITIAL_MEASURE)).onJS as unknown as MeasuredDimensions;
const measuredFinal = (await getRegisteredValue(FINAL_MEASURE)).onJS as unknown as MeasuredDimensions;

// Check the distance from the top
const finalStyleFull = { ...DEFAULT_STYLE, ...finalStyle };
const initialStyleFull = { ...DEFAULT_STYLE, ...initialStyle };

expect(measuredFinal.height).toBeWithinRange(finalStyleFull.height - 2, finalStyleFull.height + 2);
expect(measuredInitial.height).toBeWithinRange(initialStyleFull.height - 2, initialStyleFull.height + 2);
expect(measuredFinal.width).toBeWithinRange(finalStyleFull.width - 2, finalStyleFull.width + 2);
expect(measuredInitial.width).toBeWithinRange(initialStyleFull.width - 2, initialStyleFull.width + 2);

const expectedFinalDistanceFromLeft = finalStyleFull.margin + finalStyleFull.left;
const expectedInitialDistanceFromLeft = initialStyleFull.margin + initialStyleFull.left;

expect(measuredFinal.x).toBeWithinRange(expectedFinalDistanceFromLeft - 3, expectedFinalDistanceFromLeft + 3);
expect(measuredInitial.x).toBeWithinRange(
expectedInitialDistanceFromLeft - 3,
expectedInitialDistanceFromLeft + 3,
);

expect(measuredFinal.pageX).toBeWithinRange(
expectedFinalDistanceFromLeft + finalParentMargin - 3,
expectedFinalDistanceFromLeft + finalParentMargin + 3,
);
expect(measuredInitial.pageX).toBeWithinRange(
expectedInitialDistanceFromLeft + initialParentMargin - 3,
expectedInitialDistanceFromLeft + initialParentMargin + 3,
);

// Unfortunately we can't directly verify the distance from the top - it relies on top bar width
// Therefore we will check that the differences between initial and final values are valid
// And the differences between absolute and relative views -as well
const expectedTopDiffRelative =
finalStyleFull.top + finalStyleFull.margin - initialStyleFull.top - initialStyleFull.margin;
const expectedTopDiffAbsolute = expectedTopDiffRelative + finalParentMargin - initialParentMargin;

expect(measuredFinal.y - measuredInitial.y).toBeWithinRange(
expectedTopDiffRelative - 2,
expectedTopDiffRelative + 2,
);

expect(measuredFinal.pageY - measuredInitial.pageY).toBeWithinRange(
expectedTopDiffAbsolute - 2,
expectedTopDiffAbsolute + 2,
);
},
);
});

describe('Test measuring component during the animation', () => {
Expand Down Expand Up @@ -168,12 +226,14 @@ describe('Test measuring component during the animation', () => {

const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
margin: DEFAULT_PARENT_MARGIN,
borderColor: 'darkseagreen',
borderWidth: 2,
borderRadius: 10,
},
smallBox: {
width: 100,
height: 50,
...DEFAULT_STYLE,
backgroundColor: 'mediumseagreen',
borderColor: 'seagreen',
borderWidth: 2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public static float[] measure(View view) {
buffer[1] -= rootY;

float result[] = new float[6];
result[0] = result[1] = 0;
result[0] = PixelUtil.toDIPFromPixel(view.getLeft());
result[1] = PixelUtil.toDIPFromPixel(view.getTop());
for (int i = 2; i < 6; ++i) result[i] = PixelUtil.toDIPFromPixel(buffer[i - 2]);

return result;
Expand Down

0 comments on commit ad3145b

Please sign in to comment.