From 1fad084345bfdc2111e1fbb1f6a12ea8e815ba8e Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Sat, 24 Oct 2026 23:01:26 +0200 Subject: [PATCH] Check transforms for skew in C++ on Android SkewMatrixHelper.isAffine2DTransformWithSkew runs before every transform update on Android 10 and later. It walked the transform array from Kotlin, importing each element and its keys over JNI, only to look for skewX/skewY. Fabric always passes a NativeArray, so read it in C++ instead, as nativeProcessTransform already does. --- .../react/uimanager/SkewMatrixHelper.kt | 5 +++ .../react/uimanager/TransformHelper.kt | 8 ++++ .../main/jni/react/jni/TransformHelper.cpp | 38 +++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/SkewMatrixHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/SkewMatrixHelper.kt index b5bb9e91ce08..94fc219de9d9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/SkewMatrixHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/SkewMatrixHelper.kt @@ -8,6 +8,7 @@ package com.facebook.react.uimanager import android.graphics.Matrix +import com.facebook.react.bridge.NativeArray import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableType @@ -28,6 +29,10 @@ internal object SkewMatrixHelper { */ @JvmStatic fun isAffine2DTransformWithSkew(transforms: ReadableArray): Boolean { + // Read Fabric's arrays in C++ rather than crossing JNI for every element and key. + if (transforms is NativeArray) { + return TransformHelper.isAffine2DTransformWithSkew(transforms) + } if (isRawMatrixShorthand(transforms)) { return false } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TransformHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TransformHelper.kt index 8c713b98fbec..62720a34a8c8 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TransformHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TransformHelper.kt @@ -237,6 +237,14 @@ public object TransformHelper { return doubleArrayOf(newTranslateX, newTranslateY, newTranslateZ) } + /** Native implementation of [SkewMatrixHelper.isAffine2DTransformWithSkew]. */ + @JvmStatic + internal fun isAffine2DTransformWithSkew(transforms: NativeArray): Boolean = + nativeIsAffine2DTransformWithSkew(transforms) + + @JvmStatic + private external fun nativeIsAffine2DTransformWithSkew(transforms: NativeArray): Boolean + @JvmStatic private external fun nativeProcessTransform( transforms: NativeArray, diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/TransformHelper.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/jni/TransformHelper.cpp index 33d8ffaa9917..222023a337f6 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/jni/TransformHelper.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/TransformHelper.cpp @@ -51,11 +51,49 @@ void processTransform( jResult->setRegion(0, MatrixSize, doubleTransform.data()); } +// Same result as SkewMatrixHelper.isAffine2DTransformWithSkew, but reads the +// array in place instead of crossing JNI for each element and key. +jboolean isAffine2DTransformWithSkew( + jni::alias_ref /*unused*/, + NativeArray* jTransforms) { + const auto& transforms = jTransforms->getArray(); + // The raw 16-element matrix shorthand used by Fabric LayoutAnimations. + if (transforms.size() == 16 && transforms[0].isNumber()) { + return JNI_FALSE; + } + bool hasSkewTransform = false; + for (const auto& transform : transforms) { + if (!transform.isObject() || transform.empty()) { + continue; + } + const auto& [key, value] = *transform.items().begin(); + if (!key.isString()) { + continue; + } + const auto& type = key.getString(); + if (type == "matrix" || type == "perspective" || type == "rotateX" || + type == "rotateY") { + return JNI_FALSE; + } + if (type == "translate") { + if (value.isArray() && value.size() > 2 && value[2].isNumber() && + value[2].asDouble() != 0.0) { + return JNI_FALSE; + } + } else if (type == "skewX" || type == "skewY") { + hasSkewTransform = true; + } + } + return hasSkewTransform ? JNI_TRUE : JNI_FALSE; +} + } // namespace void TransformHelper::registerNatives() { javaClassLocal()->registerNatives({ makeNativeMethod("nativeProcessTransform", processTransform), + makeNativeMethod( + "nativeIsAffine2DTransformWithSkew", isAffine2DTransformWithSkew), }); }