From 6e271b8c27545694888ebd4bf06d70c2a473179e Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 21 May 2026 10:43:27 +0200 Subject: [PATCH] Accept percentage-based border radius --- .../RNGestureHandlerButtonViewManager.kt | 38 ++++++++++--------- .../RNGestureHandlerButtonNativeComponent.ts | 34 +++++++++++------ 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt index f5cf765b3d..2205c3349d 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt @@ -25,10 +25,10 @@ import android.view.accessibility.AccessibilityNodeInfo import androidx.core.view.children import androidx.interpolator.view.animation.FastOutSlowInInterpolator import com.facebook.react.R +import com.facebook.react.bridge.Dynamic import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.BackgroundStyleApplicator import com.facebook.react.uimanager.LengthPercentage -import com.facebook.react.uimanager.LengthPercentageType import com.facebook.react.uimanager.PixelUtil import com.facebook.react.uimanager.PointerEvents import com.facebook.react.uimanager.ReactPointerEventsView @@ -178,74 +178,76 @@ class RNGestureHandlerButtonViewManager : view.setOverflow(overflow) } - private fun setBorderRadiusInternal(view: ButtonViewGroup, prop: BorderRadiusProp, value: Float) { - val isUnset = value.isNaN() || value < 0f - val lp = if (isUnset) null else LengthPercentage(value, LengthPercentageType.POINT) + private fun setBorderRadiusInternal(view: ButtonViewGroup, prop: BorderRadiusProp, value: Dynamic) { + // setFromDynamic returns null for null Dynamics, negative numbers, and + // unparseable strings — which is what we want for "unset" so that + // general / physical radii continue to cascade. + val lp = LengthPercentage.setFromDynamic(value) BackgroundStyleApplicator.setBorderRadius(view, prop, lp) } @ReactProp(name = ViewProps.BORDER_RADIUS) - override fun setBorderRadius(view: ButtonViewGroup, borderRadius: Float) { - setBorderRadiusInternal(view, BorderRadiusProp.BORDER_RADIUS, borderRadius) + override fun setBorderRadius(view: ButtonViewGroup, value: Dynamic) { + setBorderRadiusInternal(view, BorderRadiusProp.BORDER_RADIUS, value) } @ReactProp(name = "borderTopLeftRadius") - override fun setBorderTopLeftRadius(view: ButtonViewGroup, value: Float) { + override fun setBorderTopLeftRadius(view: ButtonViewGroup, value: Dynamic) { setBorderRadiusInternal(view, BorderRadiusProp.BORDER_TOP_LEFT_RADIUS, value) } @ReactProp(name = "borderTopRightRadius") - override fun setBorderTopRightRadius(view: ButtonViewGroup, value: Float) { + override fun setBorderTopRightRadius(view: ButtonViewGroup, value: Dynamic) { setBorderRadiusInternal(view, BorderRadiusProp.BORDER_TOP_RIGHT_RADIUS, value) } @ReactProp(name = "borderBottomRightRadius") - override fun setBorderBottomRightRadius(view: ButtonViewGroup, value: Float) { + override fun setBorderBottomRightRadius(view: ButtonViewGroup, value: Dynamic) { setBorderRadiusInternal(view, BorderRadiusProp.BORDER_BOTTOM_RIGHT_RADIUS, value) } @ReactProp(name = "borderBottomLeftRadius") - override fun setBorderBottomLeftRadius(view: ButtonViewGroup, value: Float) { + override fun setBorderBottomLeftRadius(view: ButtonViewGroup, value: Dynamic) { setBorderRadiusInternal(view, BorderRadiusProp.BORDER_BOTTOM_LEFT_RADIUS, value) } @ReactProp(name = "borderTopStartRadius") - override fun setBorderTopStartRadius(view: ButtonViewGroup, value: Float) { + override fun setBorderTopStartRadius(view: ButtonViewGroup, value: Dynamic) { setBorderRadiusInternal(view, BorderRadiusProp.BORDER_TOP_START_RADIUS, value) } @ReactProp(name = "borderTopEndRadius") - override fun setBorderTopEndRadius(view: ButtonViewGroup, value: Float) { + override fun setBorderTopEndRadius(view: ButtonViewGroup, value: Dynamic) { setBorderRadiusInternal(view, BorderRadiusProp.BORDER_TOP_END_RADIUS, value) } @ReactProp(name = "borderBottomStartRadius") - override fun setBorderBottomStartRadius(view: ButtonViewGroup, value: Float) { + override fun setBorderBottomStartRadius(view: ButtonViewGroup, value: Dynamic) { setBorderRadiusInternal(view, BorderRadiusProp.BORDER_BOTTOM_START_RADIUS, value) } @ReactProp(name = "borderBottomEndRadius") - override fun setBorderBottomEndRadius(view: ButtonViewGroup, value: Float) { + override fun setBorderBottomEndRadius(view: ButtonViewGroup, value: Dynamic) { setBorderRadiusInternal(view, BorderRadiusProp.BORDER_BOTTOM_END_RADIUS, value) } @ReactProp(name = "borderEndEndRadius") - override fun setBorderEndEndRadius(view: ButtonViewGroup, value: Float) { + override fun setBorderEndEndRadius(view: ButtonViewGroup, value: Dynamic) { setBorderRadiusInternal(view, BorderRadiusProp.BORDER_END_END_RADIUS, value) } @ReactProp(name = "borderEndStartRadius") - override fun setBorderEndStartRadius(view: ButtonViewGroup, value: Float) { + override fun setBorderEndStartRadius(view: ButtonViewGroup, value: Dynamic) { setBorderRadiusInternal(view, BorderRadiusProp.BORDER_END_START_RADIUS, value) } @ReactProp(name = "borderStartEndRadius") - override fun setBorderStartEndRadius(view: ButtonViewGroup, value: Float) { + override fun setBorderStartEndRadius(view: ButtonViewGroup, value: Dynamic) { setBorderRadiusInternal(view, BorderRadiusProp.BORDER_START_END_RADIUS, value) } @ReactProp(name = "borderStartStartRadius") - override fun setBorderStartStartRadius(view: ButtonViewGroup, value: Float) { + override fun setBorderStartStartRadius(view: ButtonViewGroup, value: Dynamic) { setBorderRadiusInternal(view, BorderRadiusProp.BORDER_START_START_RADIUS, value) } diff --git a/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts b/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts index 79c71f2f6c..a1430f5ac4 100644 --- a/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts +++ b/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts @@ -2,6 +2,7 @@ import type { ColorValue, ViewProps } from 'react-native'; import type { Float, Int32, + UnsafeMixed, WithDefault, } from 'react-native/Libraries/Types/CodegenTypes'; import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; @@ -56,17 +57,28 @@ interface NativeProps extends ViewProps { borderBlockEndColor?: ColorValue; borderBlockStartColor?: ColorValue; - // Border radius — logical variants beyond what ViewProps provides - // WithDefault -1 so the codegen sends -1 (our "unset" sentinel) instead of 0 - // when the prop is absent, letting physical / general radii take effect. - borderTopStartRadius?: WithDefault; - borderTopEndRadius?: WithDefault; - borderBottomStartRadius?: WithDefault; - borderBottomEndRadius?: WithDefault; - borderEndEndRadius?: WithDefault; - borderEndStartRadius?: WithDefault; - borderStartEndRadius?: WithDefault; - borderStartStartRadius?: WithDefault; + // Border radius — declared as UnsafeMixed (folly::dynamic on iOS, + // DynamicFromObject on Android) so codegen forwards the raw value + // without coercing to Float. This lets the Android view manager parse + // both numeric points and percentage strings via + // LengthPercentage.setFromDynamic, matching RN's standard View. The + // non-logical variants are declared explicitly so they're dispatched + // through our delegate instead of falling through to + // BaseViewManagerDelegate, which casts to Double and would crash on a + // string value. + borderRadius?: UnsafeMixed; + borderTopLeftRadius?: UnsafeMixed; + borderTopRightRadius?: UnsafeMixed; + borderBottomLeftRadius?: UnsafeMixed; + borderBottomRightRadius?: UnsafeMixed; + borderTopStartRadius?: UnsafeMixed; + borderTopEndRadius?: UnsafeMixed; + borderBottomStartRadius?: UnsafeMixed; + borderBottomEndRadius?: UnsafeMixed; + borderEndEndRadius?: UnsafeMixed; + borderEndStartRadius?: UnsafeMixed; + borderStartEndRadius?: UnsafeMixed; + borderStartStartRadius?: UnsafeMixed; } export default codegenNativeComponent('RNGestureHandlerButton');