Skip to content

Commit

Permalink
Add border radii to snapshots (#5988)
Browse files Browse the repository at this point in the history
## Summary
This PR adds the possibility to animate the border radius of all 4
corners separately with shared elements transitions.

## Test plan

Check the behavior of `BorderRadiiExample` and also check for
regressions in other SET/LA examples.
  • Loading branch information
bartlomiejbloniarz committed May 10, 2024
1 parent 0c962f9 commit 6cb1a66
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,68 @@
import com.facebook.react.views.image.ReactImageView;
import com.facebook.react.views.view.ReactViewBackgroundDrawable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReactNativeUtils {

private static Field mBorderRadiusField;
private static Method getCornerRadiiMethod;

public static float getBorderRadius(View view) {
public static class BorderRadii {
public float full, topLeft, topRight, bottomLeft, bottomRight;

public BorderRadii(
float full, float topLeft, float topRight, float bottomLeft, float bottomRight) {
this.full = Float.isNaN(full) ? 0 : full;
this.topLeft = Float.isNaN(topLeft) ? this.full : topLeft;
this.topRight = Float.isNaN(topRight) ? this.full : topRight;
this.bottomLeft = Float.isNaN(bottomLeft) ? this.full : bottomLeft;
this.bottomRight = Float.isNaN(bottomRight) ? this.full : bottomRight;
}
}

public static BorderRadii getBorderRadii(View view) {
if (view.getBackground() != null) {
Drawable background = view.getBackground();
if (background instanceof ReactViewBackgroundDrawable) {
return ((ReactViewBackgroundDrawable) background).getFullBorderRadius();
ReactViewBackgroundDrawable drawable = (ReactViewBackgroundDrawable) background;
return new BorderRadii(
drawable.getFullBorderRadius(),
drawable.getBorderRadius(ReactViewBackgroundDrawable.BorderRadiusLocation.TOP_LEFT),
drawable.getBorderRadius(ReactViewBackgroundDrawable.BorderRadiusLocation.TOP_RIGHT),
drawable.getBorderRadius(ReactViewBackgroundDrawable.BorderRadiusLocation.BOTTOM_LEFT),
drawable.getBorderRadius(
ReactViewBackgroundDrawable.BorderRadiusLocation.BOTTOM_RIGHT));
}
} else if (view instanceof ReactImageView) {
try {
if (mBorderRadiusField == null) {
mBorderRadiusField = ReactImageView.class.getDeclaredField("mBorderRadius");
mBorderRadiusField.setAccessible(true);
}
float borderRadius = mBorderRadiusField.getFloat(view);
if (Float.isNaN(borderRadius)) {
return 0;
float fullBorderRadius = mBorderRadiusField.getFloat(view);
if (getCornerRadiiMethod == null) {
getCornerRadiiMethod =
ReactImageView.class.getDeclaredMethod("getCornerRadii", float[].class);
getCornerRadiiMethod.setAccessible(true);
}
if (Float.isNaN(fullBorderRadius)) {
fullBorderRadius = 0;
}
return borderRadius;
} catch (NullPointerException | NoSuchFieldException | IllegalAccessException ignored) {
float[] cornerRadii = new float[4];
getCornerRadiiMethod.invoke(view, (Object) cornerRadii);
return new BorderRadii(
fullBorderRadius, cornerRadii[0], cornerRadii[1], cornerRadii[2], cornerRadii[3]);
} catch (NullPointerException
| NoSuchFieldException
| NoSuchMethodException
| IllegalAccessException
| InvocationTargetException ignored) {
// In case of non-standard view is better to not support the border animation
// instead of throwing exception
}
}
return 0;
return new BorderRadii(0, 0, 0, 0, 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public class Snapshot {
public static final String GLOBAL_ORIGIN_X = "globalOriginX";
public static final String GLOBAL_ORIGIN_Y = "globalOriginY";
public static final String BORDER_RADIUS = "borderRadius";
public static final String BORDER_TOP_LEFT_RADIUS = "borderTopLeftRadius";
public static final String BORDER_TOP_RIGHT_RADIUS = "borderTopRightRadius";
public static final String BORDER_BOTTOM_LEFT_RADIUS = "borderBottomLeftRadius";
public static final String BORDER_BOTTOM_RIGHT_RADIUS = "borderBottomRightRadius";

public static final String CURRENT_WIDTH = "currentWidth";
public static final String CURRENT_HEIGHT = "currentHeight";
Expand All @@ -29,6 +33,10 @@ public class Snapshot {
public static final String CURRENT_GLOBAL_ORIGIN_X = "currentGlobalOriginX";
public static final String CURRENT_GLOBAL_ORIGIN_Y = "currentGlobalOriginY";
public static final String CURRENT_BORDER_RADIUS = "currentBorderRadius";
public static final String CURRENT_BORDER_TOP_LEFT_RADIUS = "currentBorderTopLeftRadius";
public static final String CURRENT_BORDER_TOP_RIGHT_RADIUS = "currentBorderTopRightRadius";
public static final String CURRENT_BORDER_BOTTOM_LEFT_RADIUS = "currentBorderBottomLeftRadius";
public static final String CURRENT_BORDER_BOTTOM_RIGHT_RADIUS = "currentBorderBottomRightRadius";

public static final String TARGET_WIDTH = "targetWidth";
public static final String TARGET_HEIGHT = "targetHeight";
Expand All @@ -38,6 +46,10 @@ public class Snapshot {
public static final String TARGET_GLOBAL_ORIGIN_X = "targetGlobalOriginX";
public static final String TARGET_GLOBAL_ORIGIN_Y = "targetGlobalOriginY";
public static final String TARGET_BORDER_RADIUS = "targetBorderRadius";
public static final String TARGET_BORDER_TOP_LEFT_RADIUS = "targetBorderTopLeftRadius";
public static final String TARGET_BORDER_TOP_RIGHT_RADIUS = "targetBorderTopRightRadius";
public static final String TARGET_BORDER_BOTTOM_LEFT_RADIUS = "targetBorderBottomLeftRadius";
public static final String TARGET_BORDER_BOTTOM_RIGHT_RADIUS = "targetBorderBottomRightRadius";

public View view;
public ViewGroup parent;
Expand All @@ -53,7 +65,7 @@ public class Snapshot {
new ArrayList<>(Arrays.asList(1f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 1f));
public int originXByParent;
public int originYByParent;
public float borderRadius;
public ReactNativeUtils.BorderRadii borderRadii;
private float[] identityMatrix = {1, 0, 0, 0, 1, 0, 0, 0, 1};

public static ArrayList<String> targetKeysToTransform =
Expand All @@ -65,7 +77,11 @@ public class Snapshot {
Snapshot.TARGET_ORIGIN_Y,
Snapshot.TARGET_GLOBAL_ORIGIN_X,
Snapshot.TARGET_GLOBAL_ORIGIN_Y,
Snapshot.TARGET_BORDER_RADIUS));
Snapshot.TARGET_BORDER_RADIUS,
Snapshot.TARGET_BORDER_TOP_LEFT_RADIUS,
Snapshot.TARGET_BORDER_TOP_RIGHT_RADIUS,
Snapshot.TARGET_BORDER_BOTTOM_LEFT_RADIUS,
Snapshot.TARGET_BORDER_BOTTOM_RIGHT_RADIUS));
public static ArrayList<String> currentKeysToTransform =
new ArrayList<>(
Arrays.asList(
Expand All @@ -75,7 +91,11 @@ public class Snapshot {
Snapshot.CURRENT_ORIGIN_Y,
Snapshot.CURRENT_GLOBAL_ORIGIN_X,
Snapshot.CURRENT_GLOBAL_ORIGIN_Y,
Snapshot.CURRENT_BORDER_RADIUS));
Snapshot.CURRENT_BORDER_RADIUS,
Snapshot.CURRENT_BORDER_TOP_LEFT_RADIUS,
Snapshot.CURRENT_BORDER_TOP_RIGHT_RADIUS,
Snapshot.CURRENT_BORDER_BOTTOM_LEFT_RADIUS,
Snapshot.CURRENT_BORDER_BOTTOM_RIGHT_RADIUS));

Snapshot(View view, NativeViewHierarchyManager viewHierarchyManager) {
parent = (ViewGroup) view.getParent();
Expand All @@ -94,6 +114,7 @@ public class Snapshot {
view.getLocationOnScreen(location);
globalOriginX = location[0];
globalOriginY = location[1];
borderRadii = new ReactNativeUtils.BorderRadii(0, 0, 0, 0, 0);
}

public Snapshot(View view) {
Expand Down Expand Up @@ -122,7 +143,7 @@ public Snapshot(View view) {
}
originXByParent = view.getLeft();
originYByParent = view.getTop();
borderRadius = ReactNativeUtils.getBorderRadius(view);
borderRadii = ReactNativeUtils.getBorderRadii(view);
}

private void addTargetConfig(HashMap<String, Object> data) {
Expand All @@ -133,7 +154,11 @@ private void addTargetConfig(HashMap<String, Object> data) {
data.put(Snapshot.TARGET_HEIGHT, height);
data.put(Snapshot.TARGET_WIDTH, width);
data.put(Snapshot.TARGET_TRANSFORM_MATRIX, transformMatrix);
data.put(Snapshot.TARGET_BORDER_RADIUS, borderRadius);
data.put(Snapshot.TARGET_BORDER_RADIUS, borderRadii.full);
data.put(Snapshot.TARGET_BORDER_TOP_LEFT_RADIUS, borderRadii.topLeft);
data.put(Snapshot.TARGET_BORDER_TOP_RIGHT_RADIUS, borderRadii.topRight);
data.put(Snapshot.TARGET_BORDER_BOTTOM_LEFT_RADIUS, borderRadii.bottomLeft);
data.put(Snapshot.TARGET_BORDER_BOTTOM_RIGHT_RADIUS, borderRadii.bottomRight);
}

private void addCurrentConfig(HashMap<String, Object> data) {
Expand All @@ -144,7 +169,11 @@ private void addCurrentConfig(HashMap<String, Object> data) {
data.put(Snapshot.CURRENT_HEIGHT, height);
data.put(Snapshot.CURRENT_WIDTH, width);
data.put(Snapshot.CURRENT_TRANSFORM_MATRIX, transformMatrix);
data.put(Snapshot.CURRENT_BORDER_RADIUS, borderRadius);
data.put(Snapshot.CURRENT_BORDER_RADIUS, borderRadii.full);
data.put(Snapshot.CURRENT_BORDER_TOP_LEFT_RADIUS, borderRadii.topLeft);
data.put(Snapshot.CURRENT_BORDER_TOP_RIGHT_RADIUS, borderRadii.topRight);
data.put(Snapshot.CURRENT_BORDER_BOTTOM_LEFT_RADIUS, borderRadii.bottomLeft);
data.put(Snapshot.CURRENT_BORDER_BOTTOM_RIGHT_RADIUS, borderRadii.bottomRight);
}

private void addBasicConfig(HashMap<String, Object> data) {
Expand All @@ -155,7 +184,11 @@ private void addBasicConfig(HashMap<String, Object> data) {
data.put(Snapshot.HEIGHT, height);
data.put(Snapshot.WIDTH, width);
data.put(Snapshot.TRANSFORM_MATRIX, transformMatrix);
data.put(Snapshot.BORDER_RADIUS, borderRadius);
data.put(Snapshot.BORDER_RADIUS, borderRadii.full);
data.put(Snapshot.BORDER_TOP_LEFT_RADIUS, borderRadii.topLeft);
data.put(Snapshot.BORDER_TOP_RIGHT_RADIUS, borderRadii.topRight);
data.put(Snapshot.BORDER_BOTTOM_LEFT_RADIUS, borderRadii.bottomLeft);
data.put(Snapshot.BORDER_BOTTOM_RIGHT_RADIUS, borderRadii.bottomRight);
}

public HashMap<String, Object> toTargetMap() {
Expand Down
109 changes: 109 additions & 0 deletions app/src/examples/SharedElementTransitions/BorderRadii.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import * as React from 'react';
import { View, Button, StyleSheet } from 'react-native';
import { ParamListBase } from '@react-navigation/native';
import {
createNativeStackNavigator,
NativeStackScreenProps,
} from '@react-navigation/native-stack';
import Animated, {
SharedTransition,
SharedTransitionType,
} from 'react-native-reanimated';

const Stack = createNativeStackNavigator();

const photo = require('./assets/image.jpg');

const transition = SharedTransition.duration(1000).defaultTransitionType(
SharedTransitionType.ANIMATION
);

function Screen1({ navigation }: NativeStackScreenProps<ParamListBase>) {
return (
<View style={styles.flexOne}>
<Button
onPress={() => navigation.navigate('Screen2')}
title="go to screen2"
/>

<Animated.View
style={styles.greenBoxScreenOne}
sharedTransitionTag="tag"
sharedTransitionStyle={transition}>
<Animated.Image
style={styles.imageOne}
sharedTransitionTag="image"
sharedTransitionStyle={transition}
source={photo}
/>
</Animated.View>
</View>
);
}

function Screen2({ navigation }: NativeStackScreenProps<ParamListBase>) {
return (
<View style={styles.flexOne}>
<Button title="go back" onPress={() => navigation.navigate('Screen1')} />
<Animated.View
style={styles.greenBoxScreenTwo}
sharedTransitionTag="tag"
sharedTransitionStyle={transition}>
<Animated.Image
style={styles.imageTwo}
sharedTransitionTag="image"
sharedTransitionStyle={transition}
source={photo}
/>
</Animated.View>
</View>
);
}

export default function CustomTransitionExample() {
return (
<Stack.Navigator>
<Stack.Screen
name="Screen1"
component={Screen1}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Screen2"
component={Screen2}
options={{ headerShown: false }}
/>
</Stack.Navigator>
);
}

const styles = StyleSheet.create({
flexOne: { flex: 1, justifyContent: 'flex-end', alignItems: 'center' },
greenBoxScreenOne: {
width: 300,
height: 150,
borderTopLeftRadius: 25,
borderTopRightRadius: 25,
backgroundColor: 'pink',
},
greenBoxScreenTwo: {
width: 350,
height: 500,
marginBottom: 100,
borderRadius: 50,
backgroundColor: 'pink',
},
imageOne: {
margin: 10,
width: 280,
height: 140,
borderTopLeftRadius: 15,
borderTopRightRadius: 15,
},
imageTwo: {
margin: 10,
width: 330,
height: 200,
borderRadius: 40,
},
});
5 changes: 5 additions & 0 deletions app/src/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ import HabitsExample from './LayoutAnimations/HabitsExample';
import MemoExample from './MemoExample';
import PerformanceMonitorExample from './PerfomanceMonitorExample';
import ScreenTransitionExample from './ScreenTransitionExample';
import BorderRadiiExample from './SharedElementTransitions/BorderRadii';
import FreezingShareablesExample from './ShareableFreezingExample';

interface Example {
Expand Down Expand Up @@ -770,4 +771,8 @@ export const EXAMPLES: Record<string, Example> = {
title: '[SET] Nested Transforms',
screen: NestedRotationExample,
},
BorderRadiiExample: {
title: '[SET] Border Radii',
screen: BorderRadiiExample,
},
} as const;
13 changes: 13 additions & 0 deletions apple/LayoutReanimation/REASharedTransitionManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -794,8 +794,21 @@ - (NSDictionary *)prepareDataForWorklet:(NSMutableDictionary *)currentValues
targetValues:targetValues];
workletValues[@"currentTransformMatrix"] = currentValues[@"combinedTransformMatrix"];
workletValues[@"targetTransformMatrix"] = targetValues[@"combinedTransformMatrix"];

workletValues[@"currentBorderRadius"] = currentValues[@"borderRadius"];
workletValues[@"targetBorderRadius"] = targetValues[@"borderRadius"];

workletValues[@"currentBorderTopLeftRadius"] = currentValues[@"borderTopLeftRadius"];
workletValues[@"targetBorderTopLeftRadius"] = targetValues[@"borderTopLeftRadius"];

workletValues[@"currentBorderTopRightRadius"] = currentValues[@"borderTopRightRadius"];
workletValues[@"targetBorderTopRightRadius"] = targetValues[@"borderTopRightRadius"];

workletValues[@"currentBorderBottomLeftRadius"] = currentValues[@"borderBottomLeftRadius"];
workletValues[@"targetBorderBottomLeftRadius"] = targetValues[@"borderBottomLeftRadius"];

workletValues[@"currentBorderBottomRightRadius"] = currentValues[@"borderBottomRightRadius"];
workletValues[@"targetBorderBottomRightRadius"] = targetValues[@"borderBottomRightRadius"];
return workletValues;
}

Expand Down
18 changes: 17 additions & 1 deletion apple/LayoutReanimation/REASnapshot.m
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,21 @@ - (void)makeSnapshotForView:(REAUIView *)view
#else
if ([view respondsToSelector:@selector(borderRadius)]) {
// For example `RCTTextView` doesn't have `borderRadius` selector
_values[@"borderRadius"] = @(((RCTView *)view).borderRadius);
RCTView *rctView = ((RCTView *)view);
CGFloat borderRadius = ((RCTView *)view).borderRadius;
_values[@"borderRadius"] = @(borderRadius);
_values[@"borderTopLeftRadius"] = @([self get:rctView.borderTopLeftRadius orDefault:borderRadius]);
_values[@"borderTopRightRadius"] = @([self get:rctView.borderTopRightRadius orDefault:borderRadius]);
_values[@"borderBottomLeftRadius"] = @([self get:rctView.borderBottomLeftRadius orDefault:borderRadius]);
_values[@"borderBottomRightRadius"] = @([self get:rctView.borderBottomRightRadius orDefault:borderRadius]);
} else {
_values[@"borderRadius"] = @(0);
_values[@"borderTopLeftRadius"] = @(0);
_values[@"borderTopRightRadius"] = @(0);
_values[@"borderBottomLeftRadius"] = @(0);
_values[@"borderBottomRightRadius"] = @(0);
}

#endif
} else {
_values[@"originX"] = @(view.center.x - view.bounds.size.width / 2.0);
Expand Down Expand Up @@ -175,6 +186,11 @@ - (REAUIView *)maybeFindTransitionView:(REAUIView *)view
return nil;
}

- (CGFloat)get:(CGFloat)value orDefault:(CGFloat)def
{
return value == -1 ? def : value;
}

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ const SUPPORTED_PROPS = [
'originY',
'transform',
'borderRadius',
'borderTopLeftRadius',
'borderTopRightRadius',
'borderBottomLeftRadius',
'borderBottomRightRadius',
] as const;

type AnimationFactory = (
Expand Down

0 comments on commit 6cb1a66

Please sign in to comment.