From 6cb1a66f1a68cac8079de2b6b305d22359847e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20B=C5=82oniarz?= <56109050+bartlomiejbloniarz@users.noreply.github.com> Date: Fri, 10 May 2024 12:03:59 +0200 Subject: [PATCH] Add border radii to snapshots (#5988) ## 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. --- .../reanimated/ReactNativeUtils.java | 51 ++++++-- .../layoutReanimation/Snapshot.java | 47 ++++++-- .../SharedElementTransitions/BorderRadii.tsx | 109 ++++++++++++++++++ app/src/examples/index.ts | 5 + .../REASharedTransitionManager.m | 13 +++ apple/LayoutReanimation/REASnapshot.m | 18 ++- .../sharedTransitions/SharedTransition.ts | 4 + 7 files changed, 231 insertions(+), 16 deletions(-) create mode 100644 app/src/examples/SharedElementTransitions/BorderRadii.tsx diff --git a/android/src/main/java/com/swmansion/reanimated/ReactNativeUtils.java b/android/src/main/java/com/swmansion/reanimated/ReactNativeUtils.java index b15ef955f3b..36676524f67 100644 --- a/android/src/main/java/com/swmansion/reanimated/ReactNativeUtils.java +++ b/android/src/main/java/com/swmansion/reanimated/ReactNativeUtils.java @@ -5,16 +5,39 @@ 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 { @@ -22,16 +45,28 @@ public static float getBorderRadius(View view) { 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); } } diff --git a/android/src/main/java/com/swmansion/reanimated/layoutReanimation/Snapshot.java b/android/src/main/java/com/swmansion/reanimated/layoutReanimation/Snapshot.java index 6e63c2fc3aa..9ebb402914a 100644 --- a/android/src/main/java/com/swmansion/reanimated/layoutReanimation/Snapshot.java +++ b/android/src/main/java/com/swmansion/reanimated/layoutReanimation/Snapshot.java @@ -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"; @@ -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"; @@ -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; @@ -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 targetKeysToTransform = @@ -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 currentKeysToTransform = new ArrayList<>( Arrays.asList( @@ -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(); @@ -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) { @@ -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 data) { @@ -133,7 +154,11 @@ private void addTargetConfig(HashMap 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 data) { @@ -144,7 +169,11 @@ private void addCurrentConfig(HashMap 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 data) { @@ -155,7 +184,11 @@ private void addBasicConfig(HashMap 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 toTargetMap() { diff --git a/app/src/examples/SharedElementTransitions/BorderRadii.tsx b/app/src/examples/SharedElementTransitions/BorderRadii.tsx new file mode 100644 index 00000000000..e74af7333b7 --- /dev/null +++ b/app/src/examples/SharedElementTransitions/BorderRadii.tsx @@ -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) { + return ( + +