diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js index 249e81199a5399..0326ec9afd621d 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js @@ -165,6 +165,7 @@ const validAttributesForNonEventProps = { // @ReactProps from BaseViewManager backgroundColor: {process: require('../StyleSheet/processColor').default}, transform: true, + transformOrigin: true, opacity: true, elevation: true, shadowColor: {process: require('../StyleSheet/processColor').default}, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index a7970346275fd7..20c3fd2d7fcefa 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -40,7 +40,7 @@ * provides support for base view properties such as backgroundColor, opacity, etc. */ public abstract class BaseViewManager - extends ViewManager implements BaseViewManagerInterface { + extends ViewManager implements BaseViewManagerInterface, View.OnLayoutChangeListener { private static final int PERSPECTIVE_ARRAY_INVERTED_CAMERA_DISTANCE_INDEX = 2; private static final float CAMERA_DISTANCE_NORMALIZATION_MULTIPLIER = (float) Math.sqrt(5); @@ -90,6 +90,10 @@ protected T prepareToRecycleView(@NonNull ThemedReactContext reactContext, T vie view.setElevation(0); view.setAnimationMatrix(null); + view.setTag(R.id.transform, null); + view.setTag(R.id.transform_origin, null); + view.setTag(R.id.invalidate_transform, null); + view.removeOnLayoutChangeListener(this); // setShadowColor if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { view.setOutlineAmbientShadowColor(Color.BLACK); @@ -129,6 +133,35 @@ protected T prepareToRecycleView(@NonNull ThemedReactContext reactContext, T vie return view; } + // Currently. onLayout listener is only attached when transform origin prop is being used. + @Override + public void onLayoutChange( + View v, + int left, + int top, + int right, + int bottom, + int oldLeft, + int oldTop, + int oldRight, + int oldBottom) { + // Old width and height + int oldWidth = oldRight - oldLeft; + int oldHeight = oldBottom - oldTop; + + // Current width and height + int currentWidth = right - left; + int currentHeight = bottom - top; + + if ((currentHeight != oldHeight || currentWidth != oldWidth)) { + ReadableArray transformOrigin = (ReadableArray) v.getTag(R.id.transform_origin); + ReadableArray transformMatrix = (ReadableArray) v.getTag(R.id.transform); + if (transformMatrix != null && transformOrigin != null) { + setTransformProperty((T) v, transformMatrix, transformOrigin); + } + } + } + @Override @ReactProp( name = ViewProps.BACKGROUND_COLOR, @@ -141,10 +174,19 @@ public void setBackgroundColor(@NonNull T view, int backgroundColor) { @Override @ReactProp(name = ViewProps.TRANSFORM) public void setTransform(@NonNull T view, @Nullable ReadableArray matrix) { - if (matrix == null) { - resetTransformProperty(view); + view.setTag(R.id.transform, matrix); + view.setTag(R.id.invalidate_transform, true); + } + + @Override + @ReactProp(name = ViewProps.TRANSFORM_ORIGIN) + public void setTransformOrigin(@NonNull T view, @Nullable ReadableArray transformOrigin) { + view.setTag(R.id.transform_origin, transformOrigin); + view.setTag(R.id.invalidate_transform, true); + if (transformOrigin != null) { + view.addOnLayoutChangeListener(this); } else { - setTransformProperty(view, matrix); + view.removeOnLayoutChangeListener(this); } } @@ -439,9 +481,15 @@ public void setAccessibilityLiveRegion(@NonNull T view, @Nullable String liveReg } } - private static void setTransformProperty(@NonNull View view, ReadableArray transforms) { + private static void setTransformProperty( + @NonNull View view, ReadableArray transforms, @Nullable ReadableArray transformOrigin) { sMatrixDecompositionContext.reset(); - TransformHelper.processTransform(transforms, sTransformDecompositionArray); + TransformHelper.processTransform( + transforms, + sTransformDecompositionArray, + PixelUtil.toDIPFromPixel(view.getWidth()), + PixelUtil.toDIPFromPixel(view.getHeight()), + transformOrigin); MatrixMathHelper.decomposeMatrix(sTransformDecompositionArray, sMatrixDecompositionContext); view.setTranslationX( PixelUtil.toPixelFromDIP( @@ -526,6 +574,17 @@ private void updateViewAccessibility(@NonNull T view) { protected void onAfterUpdateTransaction(@NonNull T view) { super.onAfterUpdateTransaction(view); updateViewAccessibility(view); + Boolean invalidateTransform = (Boolean) view.getTag(R.id.invalidate_transform); + if (invalidateTransform != null && invalidateTransform) { + ReadableArray transformOrigin = (ReadableArray) view.getTag(R.id.transform_origin); + ReadableArray transformMatrix = (ReadableArray) view.getTag(R.id.transform); + if (transformMatrix != null) { + setTransformProperty(view, transformMatrix, transformOrigin); + } else { + resetTransformProperty(view); + } + view.setTag(R.id.invalidate_transform, false); + } } @Override diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.java index e59a1c4da3b80f..2a845e1903c59d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.java @@ -121,6 +121,9 @@ public void setProperty(T view, String propName, @Nullable Object value) { case ViewProps.TRANSFORM: mViewManager.setTransform(view, (ReadableArray) value); break; + case ViewProps.TRANSFORM_ORIGIN: + mViewManager.setTransformOrigin(view, (ReadableArray) value); + break; case ViewProps.TRANSLATE_X: mViewManager.setTranslateX(view, value == null ? 0.0f : ((Double) value).floatValue()); break; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerInterface.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerInterface.java index 5887ff5ba31535..6ef232813dbc3f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerInterface.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerInterface.java @@ -72,6 +72,8 @@ public interface BaseViewManagerInterface { void setTransform(T view, @Nullable ReadableArray matrix); + void setTransformOrigin(T view, @Nullable ReadableArray transformOrigin); + void setTranslateX(T view, float translateX); void setTranslateY(T view, float translateY); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TransformHelper.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TransformHelper.java index 49d6af12396a01..5a5a9ddaa92750 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TransformHelper.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TransformHelper.java @@ -46,65 +46,121 @@ private static double convertToRadians(ReadableMap transformMap, String key) { } public static void processTransform(ReadableArray transforms, double[] result) { + processTransform(transforms, result, 0, 0, null); + } + + public static void processTransform( + ReadableArray transforms, + double[] result, + float viewWidth, + float viewHeight, + ReadableArray transformOrigin) { double[] helperMatrix = sHelperMatrix.get(); MatrixMathHelper.resetIdentityMatrix(result); + float[] offsets = getTranslateForTransformOrigin(viewWidth, viewHeight, transformOrigin); + if (offsets != null) { + MatrixMathHelper.resetIdentityMatrix(helperMatrix); + MatrixMathHelper.applyTranslate3D(helperMatrix, offsets[0], offsets[1], offsets[2]); + MatrixMathHelper.multiplyInto(result, result, helperMatrix); + } // If the transforms array is actually just the matrix itself, // copy that directly. This is for Fabric LayoutAnimations support. // All of the stuff this Java helper does is already done in C++ in Fabric, so we // can just use that matrix directly. if (transforms.size() == 16 && transforms.getType(0) == ReadableType.Number) { + MatrixMathHelper.resetIdentityMatrix(helperMatrix); for (int i = 0; i < transforms.size(); i++) { - result[i] = transforms.getDouble(i); + helperMatrix[i] = transforms.getDouble(i); } - return; - } - - for (int transformIdx = 0, size = transforms.size(); transformIdx < size; transformIdx++) { - ReadableMap transform = transforms.getMap(transformIdx); - String transformType = transform.keySetIterator().nextKey(); + MatrixMathHelper.multiplyInto(result, result, helperMatrix); + } else { + for (int transformIdx = 0, size = transforms.size(); transformIdx < size; transformIdx++) { + ReadableMap transform = transforms.getMap(transformIdx); + String transformType = transform.keySetIterator().nextKey(); - MatrixMathHelper.resetIdentityMatrix(helperMatrix); - if ("matrix".equals(transformType)) { - ReadableArray matrix = transform.getArray(transformType); - for (int i = 0; i < 16; i++) { - helperMatrix[i] = matrix.getDouble(i); + MatrixMathHelper.resetIdentityMatrix(helperMatrix); + if ("matrix".equals(transformType)) { + ReadableArray matrix = transform.getArray(transformType); + for (int i = 0; i < 16; i++) { + helperMatrix[i] = matrix.getDouble(i); + } + } else if ("perspective".equals(transformType)) { + MatrixMathHelper.applyPerspective(helperMatrix, transform.getDouble(transformType)); + } else if ("rotateX".equals(transformType)) { + MatrixMathHelper.applyRotateX(helperMatrix, convertToRadians(transform, transformType)); + } else if ("rotateY".equals(transformType)) { + MatrixMathHelper.applyRotateY(helperMatrix, convertToRadians(transform, transformType)); + } else if ("rotate".equals(transformType) || "rotateZ".equals(transformType)) { + MatrixMathHelper.applyRotateZ(helperMatrix, convertToRadians(transform, transformType)); + } else if ("scale".equals(transformType)) { + double scale = transform.getDouble(transformType); + MatrixMathHelper.applyScaleX(helperMatrix, scale); + MatrixMathHelper.applyScaleY(helperMatrix, scale); + } else if ("scaleX".equals(transformType)) { + MatrixMathHelper.applyScaleX(helperMatrix, transform.getDouble(transformType)); + } else if ("scaleY".equals(transformType)) { + MatrixMathHelper.applyScaleY(helperMatrix, transform.getDouble(transformType)); + } else if ("translate".equals(transformType)) { + ReadableArray value = transform.getArray(transformType); + double x = value.getDouble(0); + double y = value.getDouble(1); + double z = value.size() > 2 ? value.getDouble(2) : 0d; + MatrixMathHelper.applyTranslate3D(helperMatrix, x, y, z); + } else if ("translateX".equals(transformType)) { + MatrixMathHelper.applyTranslate2D(helperMatrix, transform.getDouble(transformType), 0d); + } else if ("translateY".equals(transformType)) { + MatrixMathHelper.applyTranslate2D(helperMatrix, 0d, transform.getDouble(transformType)); + } else if ("skewX".equals(transformType)) { + MatrixMathHelper.applySkewX(helperMatrix, convertToRadians(transform, transformType)); + } else if ("skewY".equals(transformType)) { + MatrixMathHelper.applySkewY(helperMatrix, convertToRadians(transform, transformType)); + } else { + FLog.w(ReactConstants.TAG, "Unsupported transform type: " + transformType); } - } else if ("perspective".equals(transformType)) { - MatrixMathHelper.applyPerspective(helperMatrix, transform.getDouble(transformType)); - } else if ("rotateX".equals(transformType)) { - MatrixMathHelper.applyRotateX(helperMatrix, convertToRadians(transform, transformType)); - } else if ("rotateY".equals(transformType)) { - MatrixMathHelper.applyRotateY(helperMatrix, convertToRadians(transform, transformType)); - } else if ("rotate".equals(transformType) || "rotateZ".equals(transformType)) { - MatrixMathHelper.applyRotateZ(helperMatrix, convertToRadians(transform, transformType)); - } else if ("scale".equals(transformType)) { - double scale = transform.getDouble(transformType); - MatrixMathHelper.applyScaleX(helperMatrix, scale); - MatrixMathHelper.applyScaleY(helperMatrix, scale); - } else if ("scaleX".equals(transformType)) { - MatrixMathHelper.applyScaleX(helperMatrix, transform.getDouble(transformType)); - } else if ("scaleY".equals(transformType)) { - MatrixMathHelper.applyScaleY(helperMatrix, transform.getDouble(transformType)); - } else if ("translate".equals(transformType)) { - ReadableArray value = transform.getArray(transformType); - double x = value.getDouble(0); - double y = value.getDouble(1); - double z = value.size() > 2 ? value.getDouble(2) : 0d; - MatrixMathHelper.applyTranslate3D(helperMatrix, x, y, z); - } else if ("translateX".equals(transformType)) { - MatrixMathHelper.applyTranslate2D(helperMatrix, transform.getDouble(transformType), 0d); - } else if ("translateY".equals(transformType)) { - MatrixMathHelper.applyTranslate2D(helperMatrix, 0d, transform.getDouble(transformType)); - } else if ("skewX".equals(transformType)) { - MatrixMathHelper.applySkewX(helperMatrix, convertToRadians(transform, transformType)); - } else if ("skewY".equals(transformType)) { - MatrixMathHelper.applySkewY(helperMatrix, convertToRadians(transform, transformType)); - } else { - FLog.w(ReactConstants.TAG, "Unsupported transform type: " + transformType); + + MatrixMathHelper.multiplyInto(result, result, helperMatrix); } + } + if (offsets != null) { + MatrixMathHelper.resetIdentityMatrix(helperMatrix); + MatrixMathHelper.applyTranslate3D(helperMatrix, -offsets[0], -offsets[1], -offsets[2]); MatrixMathHelper.multiplyInto(result, result, helperMatrix); } } + + private static float[] getTranslateForTransformOrigin( + float viewWidth, float viewHeight, ReadableArray transformOrigin) { + if (transformOrigin == null || (viewHeight == 0 && viewWidth == 0)) { + return null; + } + float viewCenterX = viewWidth / 2; + float viewCenterY = viewHeight / 2; + + float[] origin = {viewCenterX, viewCenterY, 0.0f}; + + for (int i = 0; i < transformOrigin.size() && i < 3; i++) { + switch (transformOrigin.getType(i)) { + case Number: + origin[i] = (float) transformOrigin.getDouble(i); + break; + case String: + { + String part = transformOrigin.getString(i); + if (part.endsWith("%")) { + float val = Float.parseFloat(part.substring(0, part.length() - 1)); + origin[i] = (i == 0 ? viewWidth : viewHeight) * val / 100.0f; + } + break; + } + } + } + + float newTranslateX = -viewCenterX + origin[0]; + float newTranslateY = -viewCenterY + origin[1]; + float newTranslateZ = origin[2]; + + return new float[] {newTranslateX, newTranslateY, newTranslateZ}; + } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java index cc9f7178e65919..fa6eae38a49771 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java @@ -151,6 +151,8 @@ public class ViewProps { public static final String ON_LAYOUT = "onLayout"; public static final String TRANSFORM = "transform"; + + public static final String TRANSFORM_ORIGIN = "transformOrigin"; public static final String ELEVATION = "elevation"; public static final String SHADOW_COLOR = "shadowColor"; public static final String Z_INDEX = "zIndex"; diff --git a/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml b/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml index d2928f810dfb90..84ebee0466979e 100644 --- a/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml +++ b/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml @@ -45,6 +45,15 @@ + + + + + + + + +