diff --git a/apps/common-app/src/new_api/components/button_underlay/index.tsx b/apps/common-app/src/new_api/components/button_underlay/index.tsx new file mode 100644 index 0000000000..1c4c54409c --- /dev/null +++ b/apps/common-app/src/new_api/components/button_underlay/index.tsx @@ -0,0 +1,552 @@ +import React from 'react'; +import { View, StyleSheet, Text, SafeAreaView } from 'react-native'; +import { + GestureHandlerRootView, + ScrollView, + RawButton, +} from 'react-native-gesture-handler'; + +const UNDERLAY_PROPS = { + underlayColor: 'red', + activeUnderlayOpacity: 0.5, + animationDuration: 200, + rippleColor: 'transparent', +} as const; + +export default function UnderlayEdgeCases() { + return ( + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + + + + + + + +
+ +
+ + + + + + + + + + + +
+ +
+ + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + +
+ +
+ + + + + + + + + + + +
+ +
+ + + + + + + + + + + +
+ +
+ + + + + + + + +
+ +
+ + + + + + + + +
+ +
+ + + + + + + + +
+
+
+
+
+ ); +} + +function Section({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( + + {title} + {children} + + ); +} + +function Row({ children }: { children: React.ReactNode }) { + return {children}; +} + +function Label({ children }: { children: string }) { + return {children}; +} + +const buttonBase = { + justifyContent: 'center' as const, + alignItems: 'center' as const, +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f5f5f5', + }, + scrollContent: { + paddingBottom: 60, + }, + padded: { + padding: 16, + }, + section: { + marginBottom: 24, + }, + sectionTitle: { + fontSize: 14, + fontWeight: 'bold', + marginBottom: 8, + color: '#333', + }, + row: { + flexDirection: 'row', + gap: 10, + marginBottom: 10, + }, + label: { + color: '#333', + fontSize: 11, + fontWeight: '600', + }, + uniformSmall: { + flex: 1, + height: 70, + backgroundColor: '#FFD61E', + borderRadius: 8, + borderWidth: 2, + borderColor: '#000', + }, + uniformLarge: { + flex: 1, + height: 70, + backgroundColor: '#38ACDD', + borderRadius: 24, + borderWidth: 2, + borderColor: '#000', + }, + uniformPill: { + flex: 1, + height: 70, + backgroundColor: '#57B495', + borderRadius: 999, + borderWidth: 2, + borderColor: '#000', + }, + perCornerMixed: { + flex: 1, + height: 80, + backgroundColor: '#FF6259', + borderTopLeftRadius: 20, + borderTopRightRadius: 4, + borderBottomLeftRadius: 4, + borderBottomRightRadius: 20, + borderWidth: 3, + borderColor: '#000', + }, + perCornerOneZero: { + flex: 1, + height: 80, + backgroundColor: '#782AEB', + borderRadius: 16, + borderTopRightRadius: 0, + borderWidth: 3, + borderColor: '#000', + }, + perCornerDiagonal: { + flex: 1, + height: 80, + backgroundColor: '#38ACDD', + borderTopLeftRadius: 30, + borderTopRightRadius: 0, + borderBottomLeftRadius: 0, + borderBottomRightRadius: 30, + borderWidth: 3, + borderColor: '#000', + }, + logicalStart: { + flex: 1, + height: 80, + backgroundColor: '#57B495', + borderTopStartRadius: 24, + borderBottomStartRadius: 24, + borderWidth: 3, + borderColor: '#000', + }, + logicalEnd: { + flex: 1, + height: 80, + backgroundColor: '#FFD61E', + borderTopEndRadius: 24, + borderBottomEndRadius: 24, + borderWidth: 3, + borderColor: '#000', + }, + logicalMixed: { + flex: 1, + height: 80, + backgroundColor: '#FF6259', + borderTopStartRadius: 20, + borderBottomEndRadius: 20, + borderTopEndRadius: 0, + borderBottomStartRadius: 0, + borderWidth: 3, + borderColor: '#000', + }, + oversizedUniform: { + flex: 1, + height: 80, + backgroundColor: '#782AEB', + borderRadius: 200, + borderWidth: 3, + borderColor: '#000', + }, + oversizedUneven: { + flex: 1, + height: 80, + backgroundColor: '#38ACDD', + borderTopLeftRadius: 200, + borderTopRightRadius: 10, + borderBottomLeftRadius: 10, + borderBottomRightRadius: 200, + borderWidth: 3, + borderColor: '#000', + }, + unevenBorderThickBottom: { + flex: 1, + height: 90, + backgroundColor: '#FFD61E', + borderRadius: 16, + borderWidth: 3, + borderBottomWidth: 16, + borderColor: '#000', + }, + unevenBorderThickLeft: { + flex: 1, + height: 90, + backgroundColor: '#57B495', + borderRadius: 16, + borderWidth: 3, + borderLeftWidth: 16, + borderColor: '#000', + }, + unevenBorderAllDifferent: { + flex: 1, + height: 90, + backgroundColor: '#FF6259', + borderRadius: 12, + borderTopWidth: 2, + borderRightWidth: 6, + borderBottomWidth: 14, + borderLeftWidth: 10, + borderColor: '#000', + }, + unevenBorderWithRadius: { + flex: 1, + height: 90, + backgroundColor: '#782AEB', + borderTopLeftRadius: 20, + borderTopRightRadius: 0, + borderBottomLeftRadius: 0, + borderBottomRightRadius: 32, + borderWidth: 5, + borderBottomWidth: 16, + borderColor: '#000', + }, + unevenOversized: { + flex: 1, + height: 90, + backgroundColor: '#38ACDD', + borderRadius: 200, + borderWidth: 3, + borderBottomWidth: 20, + borderColor: '#000', + }, + unevenOversizedMixed: { + flex: 1, + height: 90, + backgroundColor: '#FFD61E', + borderRadius: 16, + borderTopRightRadius: 0, + borderBottomRightRadius: 32, + borderWidth: 5, + borderBottomWidth: 16, + borderColor: '#000', + }, + noBorderRadius: { + flex: 1, + height: 70, + backgroundColor: '#57B495', + borderWidth: 3, + borderColor: '#000', + }, + zeroBorderRadius: { + flex: 1, + height: 70, + backgroundColor: '#FF6259', + borderRadius: 0, + borderWidth: 3, + borderColor: '#000', + }, + someZeroCorners: { + flex: 1, + height: 70, + backgroundColor: '#782AEB', + borderRadius: 16, + borderTopLeftRadius: 0, + borderBottomRightRadius: 0, + borderWidth: 3, + borderColor: '#000', + }, + radiusLessThanBorder: { + flex: 1, + height: 80, + backgroundColor: '#38ACDD', + borderRadius: 5, + borderWidth: 10, + borderColor: '#000', + }, + radiusEqualsBorder: { + flex: 1, + height: 80, + backgroundColor: '#FFD61E', + borderRadius: 10, + borderWidth: 10, + borderColor: '#000', + }, + radiusSlightlyMore: { + flex: 1, + height: 80, + backgroundColor: '#57B495', + borderRadius: 12, + borderWidth: 10, + borderColor: '#000', + }, + ellipticalInner: { + flex: 1, + height: 90, + backgroundColor: '#FF6259', + borderRadius: 20, + borderWidth: 5, + borderBottomWidth: 16, + borderColor: '#000', + }, + ellipticalInnerLarge: { + flex: 1, + height: 90, + backgroundColor: '#782AEB', + borderRadius: 24, + borderWidth: 2, + borderBottomWidth: 20, + borderColor: '#000', + }, + noBorderWithRadius: { + flex: 1, + height: 70, + backgroundColor: '#38ACDD', + borderRadius: 16, + }, + noBorderPill: { + flex: 1, + height: 70, + backgroundColor: '#57B495', + borderRadius: 999, + }, + paddingSmallRadius: { + flex: 1, + height: 80, + backgroundColor: '#FFD61E', + borderRadius: 8, + borderWidth: 3, + borderColor: '#000', + padding: 16, + }, + paddingLargeRadius: { + flex: 1, + height: 80, + backgroundColor: '#FF6259', + borderRadius: 30, + borderWidth: 3, + borderBottomWidth: 12, + borderColor: '#000', + padding: 16, + }, +}); diff --git a/apps/common-app/src/new_api/index.tsx b/apps/common-app/src/new_api/index.tsx index 3c706cf91b..5a9f73e41b 100644 --- a/apps/common-app/src/new_api/index.tsx +++ b/apps/common-app/src/new_api/index.tsx @@ -27,6 +27,7 @@ import RotationExample from './simple/rotation'; import TapExample from './simple/tap'; import ButtonsExample from './components/buttons'; +import ButtonUnderlayExample from './components/button_underlay'; import ReanimatedDrawerLayout from './components/drawer'; import FlatListExample from './components/flatlist'; import ScrollViewExample from './components/scrollview'; @@ -105,6 +106,7 @@ export const NEW_EXAMPLES: ExamplesSection[] = [ { name: 'FlatList example', component: FlatListExample }, { name: 'ScrollView example', component: ScrollViewExample }, { name: 'Buttons example', component: ButtonsExample }, + { name: 'Button underlay example', component: ButtonUnderlayExample }, { name: 'Switch & TextInput', component: SwitchTextInputExample }, { name: 'Reanimated Swipeable', component: Swipeable }, { name: 'Reanimated Drawer Layout', component: ReanimatedDrawerLayout }, diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/RNGestureHandlerPackage.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/RNGestureHandlerPackage.kt index d5c7001e20..95f071671f 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/RNGestureHandlerPackage.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/RNGestureHandlerPackage.kt @@ -11,7 +11,6 @@ import com.facebook.react.module.model.ReactModuleInfo import com.facebook.react.module.model.ReactModuleInfoProvider import com.facebook.react.uimanager.ViewManager import com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager -import com.swmansion.gesturehandler.react.RNGestureHandlerButtonWrapperViewManager import com.swmansion.gesturehandler.react.RNGestureHandlerDetectorViewManager import com.swmansion.gesturehandler.react.RNGestureHandlerModule import com.swmansion.gesturehandler.react.RNGestureHandlerRootViewManager @@ -35,9 +34,6 @@ class RNGestureHandlerPackage : RNGestureHandlerDetectorViewManager.REACT_CLASS to ModuleSpec.viewManagerSpec { RNGestureHandlerDetectorViewManager() }, - RNGestureHandlerButtonWrapperViewManager.REACT_CLASS to ModuleSpec.viewManagerSpec { - RNGestureHandlerButtonWrapperViewManager() - }, ) } @@ -45,7 +41,6 @@ class RNGestureHandlerPackage : RNGestureHandlerRootViewManager(), RNGestureHandlerButtonViewManager(), RNGestureHandlerDetectorViewManager(), - RNGestureHandlerButtonWrapperViewManager(), ) override fun getViewManagerNames(reactContext: ReactApplicationContext) = viewManagers.keys.toList() 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 7a013ad041..31428bed99 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 @@ -7,12 +7,9 @@ import android.annotation.SuppressLint import android.annotation.TargetApi import android.content.Context import android.content.res.ColorStateList +import android.graphics.Canvas import android.graphics.Color -import android.graphics.DashPathEffect -import android.graphics.Paint -import android.graphics.PathEffect import android.graphics.drawable.Drawable -import android.graphics.drawable.LayerDrawable import android.graphics.drawable.PaintDrawable import android.graphics.drawable.RippleDrawable import android.graphics.drawable.ShapeDrawable @@ -28,6 +25,9 @@ import androidx.core.view.children import androidx.interpolator.view.animation.LinearOutSlowInInterpolator import com.facebook.react.R 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 @@ -36,6 +36,9 @@ import com.facebook.react.uimanager.ViewGroupManager import com.facebook.react.uimanager.ViewManagerDelegate import com.facebook.react.uimanager.ViewProps import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.uimanager.style.BorderRadiusProp +import com.facebook.react.uimanager.style.BorderStyle +import com.facebook.react.uimanager.style.LogicalEdge import com.facebook.react.viewmanagers.RNGestureHandlerButtonManagerDelegate import com.facebook.react.viewmanagers.RNGestureHandlerButtonManagerInterface import com.swmansion.gesturehandler.core.NativeViewGestureHandler @@ -63,7 +66,7 @@ class RNGestureHandlerButtonViewManager : @ReactProp(name = "backgroundColor") override fun setBackgroundColor(view: ButtonViewGroup, backgroundColor: Int) { - view.setBackgroundColor(backgroundColor) + BackgroundStyleApplicator.setBackgroundColor(view, backgroundColor) } @ReactProp(name = "borderless") @@ -76,44 +79,171 @@ class RNGestureHandlerButtonViewManager : view.isEnabled = enabled } + @ReactProp(name = "borderWidth") + override fun setBorderWidth(view: ButtonViewGroup, borderWidth: Float) { + BackgroundStyleApplicator.setBorderWidth(view, LogicalEdge.ALL, borderWidth) + } + + @ReactProp(name = "borderLeftWidth") + override fun setBorderLeftWidth(view: ButtonViewGroup, value: Float) { + BackgroundStyleApplicator.setBorderWidth(view, LogicalEdge.LEFT, value) + } + + @ReactProp(name = "borderRightWidth") + override fun setBorderRightWidth(view: ButtonViewGroup, value: Float) { + BackgroundStyleApplicator.setBorderWidth(view, LogicalEdge.RIGHT, value) + } + + @ReactProp(name = "borderTopWidth") + override fun setBorderTopWidth(view: ButtonViewGroup, value: Float) { + BackgroundStyleApplicator.setBorderWidth(view, LogicalEdge.TOP, value) + } + + @ReactProp(name = "borderBottomWidth") + override fun setBorderBottomWidth(view: ButtonViewGroup, value: Float) { + BackgroundStyleApplicator.setBorderWidth(view, LogicalEdge.BOTTOM, value) + } + + @ReactProp(name = "borderStartWidth") + override fun setBorderStartWidth(view: ButtonViewGroup, value: Float) { + BackgroundStyleApplicator.setBorderWidth(view, LogicalEdge.START, value) + } + + @ReactProp(name = "borderEndWidth") + override fun setBorderEndWidth(view: ButtonViewGroup, value: Float) { + BackgroundStyleApplicator.setBorderWidth(view, LogicalEdge.END, value) + } + + @ReactProp(name = "borderColor") + override fun setBorderColor(view: ButtonViewGroup, borderColor: Int?) { + BackgroundStyleApplicator.setBorderColor(view, LogicalEdge.ALL, borderColor) + } + + @ReactProp(name = "borderLeftColor") + override fun setBorderLeftColor(view: ButtonViewGroup, value: Int?) { + BackgroundStyleApplicator.setBorderColor(view, LogicalEdge.LEFT, value) + } + + @ReactProp(name = "borderRightColor") + override fun setBorderRightColor(view: ButtonViewGroup, value: Int?) { + BackgroundStyleApplicator.setBorderColor(view, LogicalEdge.RIGHT, value) + } + + @ReactProp(name = "borderTopColor") + override fun setBorderTopColor(view: ButtonViewGroup, value: Int?) { + BackgroundStyleApplicator.setBorderColor(view, LogicalEdge.TOP, value) + } + + @ReactProp(name = "borderBottomColor") + override fun setBorderBottomColor(view: ButtonViewGroup, value: Int?) { + BackgroundStyleApplicator.setBorderColor(view, LogicalEdge.BOTTOM, value) + } + + @ReactProp(name = "borderStartColor") + override fun setBorderStartColor(view: ButtonViewGroup, value: Int?) { + BackgroundStyleApplicator.setBorderColor(view, LogicalEdge.START, value) + } + + @ReactProp(name = "borderEndColor") + override fun setBorderEndColor(view: ButtonViewGroup, value: Int?) { + BackgroundStyleApplicator.setBorderColor(view, LogicalEdge.END, value) + } + + @ReactProp(name = "borderBlockColor") + override fun setBorderBlockColor(view: ButtonViewGroup, value: Int?) { + BackgroundStyleApplicator.setBorderColor(view, LogicalEdge.BLOCK, value) + } + + @ReactProp(name = "borderBlockEndColor") + override fun setBorderBlockEndColor(view: ButtonViewGroup, value: Int?) { + BackgroundStyleApplicator.setBorderColor(view, LogicalEdge.BLOCK_END, value) + } + + @ReactProp(name = "borderBlockStartColor") + override fun setBorderBlockStartColor(view: ButtonViewGroup, value: Int?) { + BackgroundStyleApplicator.setBorderColor(view, LogicalEdge.BLOCK_START, value) + } + + @ReactProp(name = "borderStyle") + override fun setBorderStyle(view: ButtonViewGroup, borderStyle: String?) { + val parsed = if (borderStyle == null) null else BorderStyle.fromString(borderStyle) + BackgroundStyleApplicator.setBorderStyle(view, parsed) + } + + @ReactProp(name = ViewProps.OVERFLOW) + override fun setOverflow(view: ButtonViewGroup, overflow: String?) { + 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) + BackgroundStyleApplicator.setBorderRadius(view, prop, lp) + } + @ReactProp(name = ViewProps.BORDER_RADIUS) override fun setBorderRadius(view: ButtonViewGroup, borderRadius: Float) { - view.borderRadius = borderRadius + setBorderRadiusInternal(view, BorderRadiusProp.BORDER_RADIUS, borderRadius) } @ReactProp(name = "borderTopLeftRadius") - override fun setBorderTopLeftRadius(view: ButtonViewGroup, borderTopLeftRadius: Float) { - view.borderTopLeftRadius = borderTopLeftRadius + override fun setBorderTopLeftRadius(view: ButtonViewGroup, value: Float) { + setBorderRadiusInternal(view, BorderRadiusProp.BORDER_TOP_LEFT_RADIUS, value) } @ReactProp(name = "borderTopRightRadius") - override fun setBorderTopRightRadius(view: ButtonViewGroup, borderTopRightRadius: Float) { - view.borderTopRightRadius = borderTopRightRadius + override fun setBorderTopRightRadius(view: ButtonViewGroup, value: Float) { + setBorderRadiusInternal(view, BorderRadiusProp.BORDER_TOP_RIGHT_RADIUS, value) + } + + @ReactProp(name = "borderBottomRightRadius") + override fun setBorderBottomRightRadius(view: ButtonViewGroup, value: Float) { + setBorderRadiusInternal(view, BorderRadiusProp.BORDER_BOTTOM_RIGHT_RADIUS, value) } @ReactProp(name = "borderBottomLeftRadius") - override fun setBorderBottomLeftRadius(view: ButtonViewGroup, borderBottomLeftRadius: Float) { - view.borderBottomLeftRadius = borderBottomLeftRadius + override fun setBorderBottomLeftRadius(view: ButtonViewGroup, value: Float) { + setBorderRadiusInternal(view, BorderRadiusProp.BORDER_BOTTOM_LEFT_RADIUS, value) } - @ReactProp(name = "borderBottomRightRadius") - override fun setBorderBottomRightRadius(view: ButtonViewGroup, borderBottomRightRadius: Float) { - view.borderBottomRightRadius = borderBottomRightRadius + @ReactProp(name = "borderTopStartRadius") + override fun setBorderTopStartRadius(view: ButtonViewGroup, value: Float) { + setBorderRadiusInternal(view, BorderRadiusProp.BORDER_TOP_START_RADIUS, value) } - @ReactProp(name = "borderWidth") - override fun setBorderWidth(view: ButtonViewGroup, borderWidth: Float) { - view.borderWidth = borderWidth + @ReactProp(name = "borderTopEndRadius") + override fun setBorderTopEndRadius(view: ButtonViewGroup, value: Float) { + setBorderRadiusInternal(view, BorderRadiusProp.BORDER_TOP_END_RADIUS, value) } - @ReactProp(name = "borderColor") - override fun setBorderColor(view: ButtonViewGroup, borderColor: Int?) { - view.borderColor = borderColor + @ReactProp(name = "borderBottomStartRadius") + override fun setBorderBottomStartRadius(view: ButtonViewGroup, value: Float) { + setBorderRadiusInternal(view, BorderRadiusProp.BORDER_BOTTOM_START_RADIUS, value) } - @ReactProp(name = "borderStyle") - override fun setBorderStyle(view: ButtonViewGroup, borderStyle: String?) { - view.borderStyle = borderStyle + @ReactProp(name = "borderBottomEndRadius") + override fun setBorderBottomEndRadius(view: ButtonViewGroup, value: Float) { + setBorderRadiusInternal(view, BorderRadiusProp.BORDER_BOTTOM_END_RADIUS, value) + } + + @ReactProp(name = "borderEndEndRadius") + override fun setBorderEndEndRadius(view: ButtonViewGroup, value: Float) { + setBorderRadiusInternal(view, BorderRadiusProp.BORDER_END_END_RADIUS, value) + } + + @ReactProp(name = "borderEndStartRadius") + override fun setBorderEndStartRadius(view: ButtonViewGroup, value: Float) { + setBorderRadiusInternal(view, BorderRadiusProp.BORDER_END_START_RADIUS, value) + } + + @ReactProp(name = "borderStartEndRadius") + override fun setBorderStartEndRadius(view: ButtonViewGroup, value: Float) { + setBorderRadiusInternal(view, BorderRadiusProp.BORDER_START_END_RADIUS, value) + } + + @ReactProp(name = "borderStartStartRadius") + override fun setBorderStartStartRadius(view: ButtonViewGroup, value: Float) { + setBorderRadiusInternal(view, BorderRadiusProp.BORDER_START_START_RADIUS, value) } @ReactProp(name = "rippleColor") @@ -214,45 +344,6 @@ class RNGestureHandlerButtonViewManager : field = useForeground } var useBorderlessDrawable = false - var borderRadius = 0f - set(radius) = withBackgroundUpdate { - field = radius * resources.displayMetrics.density - } - var borderTopLeftRadius = 0f - set(radius) = withBackgroundUpdate { - field = radius * resources.displayMetrics.density - } - var borderTopRightRadius = 0f - set(radius) = withBackgroundUpdate { - field = radius * resources.displayMetrics.density - } - var borderBottomLeftRadius = 0f - set(radius) = withBackgroundUpdate { - field = radius * resources.displayMetrics.density - } - var borderBottomRightRadius = 0f - set(radius) = withBackgroundUpdate { - field = radius * resources.displayMetrics.density - } - var borderWidth = 0f - set(width) = withBackgroundUpdate { - field = width * resources.displayMetrics.density - } - var borderColor: Int? = null - set(color) = withBackgroundUpdate { - field = color - } - var borderStyle: String? = "solid" - set(style) = withBackgroundUpdate { - field = style - } - - private val hasBorderRadii: Boolean - get() = borderRadius != 0f || - borderTopLeftRadius != 0f || - borderTopRightRadius != 0f || - borderBottomLeftRadius != 0f || - borderBottomRightRadius != 0f var exclusive = true var animationDuration: Int = 100 @@ -272,7 +363,6 @@ class RNGestureHandlerButtonViewManager : override var pointerEvents: PointerEvents = PointerEvents.AUTO - private var buttonBackgroundColor = Color.TRANSPARENT private var needBackgroundUpdate = false private var lastEventTime = -1L private var lastAction = -1 @@ -280,6 +370,10 @@ class RNGestureHandlerButtonViewManager : private var currentAnimator: AnimatorSet? = null private var underlayDrawable: PaintDrawable? = null + // When non-null the ripple is drawn in dispatchDraw (above background, below children). + // When null the ripple lives on the foreground drawable instead. + private var selectableDrawable: Drawable? = null + var isTouched = false init { @@ -296,30 +390,22 @@ class RNGestureHandlerButtonViewManager : needBackgroundUpdate = true } - private fun buildBorderRadii(): FloatArray { - // duplicate radius for each corner, as setCornerRadii expects X radius and Y radius for each - return floatArrayOf( - borderTopLeftRadius, - borderTopLeftRadius, - borderTopRightRadius, - borderTopRightRadius, - borderBottomRightRadius, - borderBottomRightRadius, - borderBottomLeftRadius, - borderBottomLeftRadius, - ) - .map { if (it != 0f) it else borderRadius } - .toFloatArray() - } - - private fun buildBorderStyle(): PathEffect? = when (borderStyle) { - "dotted" -> DashPathEffect(floatArrayOf(borderWidth, borderWidth, borderWidth, borderWidth), 0f) - "dashed" -> DashPathEffect(floatArrayOf(borderWidth * 3, borderWidth * 3, borderWidth * 3, borderWidth * 3), 0f) - else -> null + fun setOverflow(overflow: String?) { + when (overflow) { + "hidden" -> { + clipChildren = true + clipToPadding = true + } + else -> { + clipChildren = false + clipToPadding = false + } + } + invalidate() } - override fun setBackgroundColor(color: Int) = withBackgroundUpdate { - buttonBackgroundColor = color + override fun setBackgroundColor(color: Int) { + BackgroundStyleApplicator.setBackgroundColor(this, color) } override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) { @@ -391,14 +477,12 @@ class RNGestureHandlerButtonViewManager : } private fun applyStartAnimationState() { - (parent as? ViewGroup)?.let { - if (activeOpacity != 1.0f || defaultOpacity != 1.0f) { - it.alpha = defaultOpacity - } - if (activeScale != 1.0f || defaultScale != 1.0f) { - it.scaleX = defaultScale - it.scaleY = defaultScale - } + if (activeOpacity != 1.0f || defaultOpacity != 1.0f) { + alpha = defaultOpacity + } + if (activeScale != 1.0f || defaultScale != 1.0f) { + scaleX = defaultScale + scaleY = defaultScale } underlayDrawable?.alpha = (defaultUnderlayOpacity * 255).toInt() } @@ -413,15 +497,12 @@ class RNGestureHandlerButtonViewManager : currentAnimator?.cancel() val animators = ArrayList() - if (hasOpacity || hasScale) { - val parent = this.parent as? ViewGroup ?: return - if (hasOpacity) { - animators.add(ObjectAnimator.ofFloat(parent, "alpha", opacity)) - } - if (hasScale) { - animators.add(ObjectAnimator.ofFloat(parent, "scaleX", scale)) - animators.add(ObjectAnimator.ofFloat(parent, "scaleY", scale)) - } + if (hasOpacity) { + animators.add(ObjectAnimator.ofFloat(this, "alpha", opacity)) + } + if (hasScale) { + animators.add(ObjectAnimator.ofFloat(this, "scaleX", scale)) + animators.add(ObjectAnimator.ofFloat(this, "scaleY", scale)) } if (hasUnderlay) { animators.add(ObjectAnimator.ofInt(underlayDrawable!!, "alpha", (underlayOpacity * 255).toInt())) @@ -444,92 +525,74 @@ class RNGestureHandlerButtonViewManager : private fun createUnderlayDrawable(): PaintDrawable { val drawable = PaintDrawable(underlayColor ?: Color.BLACK) - if (hasBorderRadii) { - drawable.setCornerRadii(buildBorderRadii()) - } drawable.alpha = (defaultUnderlayOpacity * 255).toInt() return drawable } - private fun updateBackgroundColor( - backgroundColor: Int, - underlay: Drawable, - borderDrawable: Drawable, - selectable: Drawable?, - ) { - val colorDrawable = PaintDrawable(backgroundColor) - - if (hasBorderRadii) { - colorDrawable.setCornerRadii(buildBorderRadii()) - } - - val layerDrawable = LayerDrawable( - if (selectable != null) { - arrayOf(colorDrawable, underlay, selectable, borderDrawable) - } else { - arrayOf(colorDrawable, underlay, borderDrawable) - }, - ) - background = layerDrawable - } - fun updateBackground() { if (!needBackgroundUpdate) { return } needBackgroundUpdate = false - if (buttonBackgroundColor == Color.TRANSPARENT) { - // reset background - background = null - } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - // reset foreground foreground = null } val selectable = createSelectableDrawable() - val borderDrawable = createBorderDrawable() val underlay = createUnderlayDrawable() underlayDrawable = underlay + // Set this view as callback so ObjectAnimator alpha changes trigger redraws. + underlay.callback = this - if (hasBorderRadii && selectable is RippleDrawable) { - val mask = PaintDrawable(Color.WHITE) - mask.setCornerRadii(buildBorderRadii()) - selectable.setDrawableByLayerId(android.R.id.mask, mask) - } - - if (useDrawableOnForeground && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (useDrawableOnForeground && selectable != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // Explicit foreground mode — View natively forwards state/hotspot. foreground = selectable - if (buttonBackgroundColor != Color.TRANSPARENT) { - updateBackgroundColor(buttonBackgroundColor, underlay, borderDrawable, null) - } - } else if (buttonBackgroundColor == Color.TRANSPARENT && rippleColor == null) { - background = LayerDrawable(arrayOf(underlay, selectable, borderDrawable)) + selectableDrawable = null } else { - updateBackgroundColor(buttonBackgroundColor, underlay, borderDrawable, selectable) + // Default — draw ripple in dispatchDraw above background, below children. + // State and hotspot are forwarded manually. + selectableDrawable = selectable + selectable?.callback = this } applyStartAnimationState() } - private fun createBorderDrawable(): Drawable { - val borderDrawable = PaintDrawable(Color.TRANSPARENT) - - if (hasBorderRadii) { - borderDrawable.setCornerRadii(buildBorderRadii()) + // Draw the underlay and ripple between background and children. + // Clip to BackgroundStyleApplicator's padding box so the overlay + // never extends beyond the view's resolved border-radius shape. + override fun dispatchDraw(canvas: Canvas) { + val hasOverlay = underlayDrawable != null || selectableDrawable != null + if (hasOverlay) { + canvas.save() + BackgroundStyleApplicator.clipToPaddingBox(this, canvas) + } + underlayDrawable?.let { + it.setBounds(0, 0, width, height) + it.draw(canvas) + } + selectableDrawable?.let { + it.setBounds(0, 0, width, height) + it.draw(canvas) } + if (hasOverlay) { + canvas.restore() + } + super.dispatchDraw(canvas) + } - if (borderWidth > 0f) { - borderDrawable.paint.apply { - style = Paint.Style.STROKE - strokeWidth = borderWidth - color = borderColor ?: Color.BLACK - pathEffect = buildBorderStyle() + override fun verifyDrawable(who: Drawable): Boolean = + super.verifyDrawable(who) || who == underlayDrawable || who == selectableDrawable + + override fun drawableStateChanged() { + super.drawableStateChanged() + // Forward pressed/enabled state to the ripple when it's drawn manually. + selectableDrawable?.let { + if (it.isStateful) { + it.setState(drawableState) } } - - return borderDrawable } private fun createSelectableDrawable(): Drawable? { @@ -563,6 +626,12 @@ class RNGestureHandlerButtonViewManager : return drawable } + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + needBackgroundUpdate = true + updateBackground() + } + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { // No-op } @@ -570,6 +639,8 @@ class RNGestureHandlerButtonViewManager : override fun drawableHotspotChanged(x: Float, y: Float) { if (touchResponder == null || touchResponder === this) { super.drawableHotspotChanged(x, y) + // Forward hotspot to the ripple when it's drawn manually. + selectableDrawable?.setHotspot(x, y) } } diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonWrapperViewManager.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonWrapperViewManager.kt deleted file mode 100644 index 7137243bd7..0000000000 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonWrapperViewManager.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.swmansion.gesturehandler.react - -import com.facebook.react.module.annotations.ReactModule -import com.facebook.react.uimanager.ThemedReactContext -import com.facebook.react.uimanager.ViewGroupManager -import com.facebook.react.uimanager.ViewManagerDelegate -import com.facebook.react.viewmanagers.RNGestureHandlerButtonWrapperManagerDelegate -import com.facebook.react.viewmanagers.RNGestureHandlerButtonWrapperManagerInterface -import com.facebook.react.views.view.ReactViewGroup - -@ReactModule(name = RNGestureHandlerButtonWrapperViewManager.REACT_CLASS) -class RNGestureHandlerButtonWrapperViewManager : - ViewGroupManager(), - RNGestureHandlerButtonWrapperManagerInterface { - private val mDelegate: ViewManagerDelegate = - RNGestureHandlerButtonWrapperManagerDelegate< - ReactViewGroup, - RNGestureHandlerButtonWrapperViewManager, - >(this) - - override fun getDelegate(): ViewManagerDelegate = mDelegate - - override fun getName() = REACT_CLASS - - override fun createViewInstance(reactContext: ThemedReactContext) = ReactViewGroup(reactContext) - - companion object { - const val REACT_CLASS = "RNGestureHandlerButtonWrapper" - } -} diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.h b/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.h index e421de01b8..2672d0b37a 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.h +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.h @@ -24,7 +24,6 @@ * Insets used when hit testing inside this view. */ @property (nonatomic, assign) UIEdgeInsets hitTestEdgeInsets; -@property (nonatomic, assign) CGFloat borderRadius; @property (nonatomic) BOOL userEnabled; @property (nonatomic, assign) RNGestureHandlerPointerEvents pointerEvents; @@ -49,6 +48,26 @@ */ - (void)applyStartAnimationState; +/** + * Updates the underlay layer's corner radii with separate horizontal/vertical + * components per corner, supporting elliptical inner corners when border widths + * are uneven. Handles both uniform and per-corner (CAShapeLayer mask) cases. + */ +- (void)setUnderlayCornerRadiiWithTopLeftHorizontal:(CGFloat)topLeftHorizontal + topLeftVertical:(CGFloat)topLeftVertical + topRightHorizontal:(CGFloat)topRightHorizontal + topRightVertical:(CGFloat)topRightVertical + bottomLeftHorizontal:(CGFloat)bottomLeftHorizontal + bottomLeftVertical:(CGFloat)bottomLeftVertical + bottomRightHorizontal:(CGFloat)bottomRightHorizontal + bottomRightVertical:(CGFloat)bottomRightVertical; + +/** + * Sets the border insets so the underlay is clipped to the padding box + * (inside the border). + */ +- (void)setUnderlayBorderInsetsWithTop:(CGFloat)top right:(CGFloat)right bottom:(CGFloat)bottom left:(CGFloat)left; + #if TARGET_OS_OSX - (void)mountChildComponentView:(RNGHUIView *)childComponentView index:(NSInteger)index; - (void)unmountChildComponentView:(RNGHUIView *)childComponentView index:(NSInteger)index; diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm index ad06c02617..078e543056 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm @@ -40,8 +40,10 @@ * controlling the touch flow. */ @implementation RNGestureHandlerButton { - CALayer *_underlayLayer; BOOL _isTouchInsideBounds; + CALayer *_underlayLayer; + CGFloat _underlayCornerRadii[8]; // [tlH, tlV, trH, trV, blH, blV, brH, brV] outer radii in points + UIEdgeInsets _underlayBorderInsets; // border widths for padding-box inset } - (void)commonInit @@ -100,21 +102,28 @@ - (void)setUnderlayColor:(RNGHColor *)underlayColor _underlayLayer.backgroundColor = underlayColor.CGColor; } +#if TARGET_OS_OSX +// Flip the macOS coordinate system so y=0 is at the top, matching iOS +// and React Native's layout expectations. +- (BOOL)isFlipped +{ + return YES; +} +#endif + #if !TARGET_OS_OSX - (void)layoutSubviews { [super layoutSubviews]; - _underlayLayer.frame = self.bounds; - [self.layer insertSublayer:_underlayLayer atIndex:0]; -} #else - (void)layout { [super layout]; - _underlayLayer.frame = self.bounds; +#endif + _underlayLayer.frame = UIEdgeInsetsInsetRect(self.bounds, _underlayBorderInsets); [self.layer insertSublayer:_underlayLayer atIndex:0]; + [self applyUnderlayCornerRadii]; } -#endif - (BOOL)shouldHandleTouch:(RNGHUIView *)view { @@ -243,6 +252,208 @@ - (void)handleAnimatePressOut } } +- (void)applyUnderlayCornerRadii +{ + CGRect rect = _underlayLayer.bounds; + CGFloat w = rect.size.width; + CGFloat h = rect.size.height; + + const CGFloat *outerTL = &_underlayCornerRadii[0]; + const CGFloat *outerTR = &_underlayCornerRadii[2]; + const CGFloat *outerBL = &_underlayCornerRadii[4]; + const CGFloat *outerBR = &_underlayCornerRadii[6]; + CGFloat borderTop = _underlayBorderInsets.top; + CGFloat borderBottom = _underlayBorderInsets.bottom; + CGFloat borderLeft = _underlayBorderInsets.left; + CGFloat borderRight = _underlayBorderInsets.right; + + // Inner border radii: outer radius minus adjacent border width per axis, clamped to 0. + CGFloat topLeftHorizontal = MAX(0, outerTL[0] - borderLeft); + CGFloat topLeftVertical = MAX(0, outerTL[1] - borderTop); + CGFloat topRightHorizontal = MAX(0, outerTR[0] - borderRight); + CGFloat topRightVertical = MAX(0, outerTR[1] - borderTop); + CGFloat bottomLeftHorizontal = MAX(0, outerBL[0] - borderLeft); + CGFloat bottomLeftVertical = MAX(0, outerBL[1] - borderBottom); + CGFloat bottomRightHorizontal = MAX(0, outerBR[0] - borderRight); + CGFloat bottomRightVertical = MAX(0, outerBR[1] - borderBottom); + + // CSS border-radius proportional scaling: if adjacent radii on any edge + // exceed that edge's length, scale all radii down by the same factor. + CGFloat f = 1.0; + if (topLeftHorizontal + topRightHorizontal > 0) + f = MIN(f, w / (topLeftHorizontal + topRightHorizontal)); + if (bottomLeftHorizontal + bottomRightHorizontal > 0) + f = MIN(f, w / (bottomLeftHorizontal + bottomRightHorizontal)); + if (topLeftVertical + bottomLeftVertical > 0) + f = MIN(f, h / (topLeftVertical + bottomLeftVertical)); + if (topRightVertical + bottomRightVertical > 0) + f = MIN(f, h / (topRightVertical + bottomRightVertical)); + + if (f < 1.0) { + topLeftHorizontal *= f; + topLeftVertical *= f; + topRightHorizontal *= f; + topRightVertical *= f; + bottomLeftHorizontal *= f; + bottomLeftVertical *= f; + bottomRightHorizontal *= f; + bottomRightVertical *= f; + } + + // Snap sub-pixel radii to 0 to avoid degenerate curves that cause + // anti-aliasing artifacts at what should be sharp corners. + if (topLeftHorizontal < 0.5) + topLeftHorizontal = 0; + if (topLeftVertical < 0.5) + topLeftVertical = 0; + if (topRightHorizontal < 0.5) + topRightHorizontal = 0; + if (topRightVertical < 0.5) + topRightVertical = 0; + if (bottomLeftHorizontal < 0.5) + bottomLeftHorizontal = 0; + if (bottomLeftVertical < 0.5) + bottomLeftVertical = 0; + if (bottomRightHorizontal < 0.5) + bottomRightHorizontal = 0; + if (bottomRightVertical < 0.5) + bottomRightVertical = 0; + + if (topLeftHorizontal == 0 && topLeftVertical == 0 && topRightHorizontal == 0 && topRightVertical == 0 && + bottomLeftHorizontal == 0 && bottomLeftVertical == 0 && bottomRightHorizontal == 0 && bottomRightVertical == 0) { + _underlayLayer.cornerRadius = 0; + _underlayLayer.mask = nil; + return; + } + + // Uniform circular — simple cornerRadius is enough + if (topLeftHorizontal == topLeftVertical && topRightHorizontal == topRightVertical && + bottomLeftHorizontal == bottomLeftVertical && bottomRightHorizontal == bottomRightVertical && + topLeftHorizontal == topRightHorizontal && topRightHorizontal == bottomLeftHorizontal && + bottomLeftHorizontal == bottomRightHorizontal) { + _underlayLayer.cornerRadius = topLeftHorizontal; + _underlayLayer.mask = nil; + return; + } + + // Non-uniform or elliptical — build a CAShapeLayer mask using cubic + // Bezier approximation for quarter-ellipse arcs (kappa ≈ 0.5523). + _underlayLayer.cornerRadius = 0; + if (CGRectIsEmpty(rect)) { + return; + } + + CGMutablePathRef path = CGPathCreateMutable(); + const CGFloat k = 0.5522847498; + BOOL hasTL = topLeftHorizontal > 0 && topLeftVertical > 0; + BOOL hasTR = topRightHorizontal > 0 && topRightVertical > 0; + BOOL hasBR = bottomRightHorizontal > 0 && bottomRightVertical > 0; + BOOL hasBL = bottomLeftHorizontal > 0 && bottomLeftVertical > 0; + + // Start at the top edge (after top-left corner if rounded) + CGPathMoveToPoint(path, NULL, hasTL ? topLeftHorizontal : 0, 0); + + // Top edge → top-right corner + if (hasTR) { + CGPathAddLineToPoint(path, NULL, w - topRightHorizontal, 0); + CGPathAddCurveToPoint( + path, + NULL, + w - topRightHorizontal + topRightHorizontal * k, + 0, + w, + topRightVertical - topRightVertical * k, + w, + topRightVertical); + } else { + CGPathAddLineToPoint(path, NULL, w, 0); + } + + // Right edge → bottom-right corner + if (hasBR) { + CGPathAddLineToPoint(path, NULL, w, h - bottomRightVertical); + CGPathAddCurveToPoint( + path, + NULL, + w, + h - bottomRightVertical + bottomRightVertical * k, + w - bottomRightHorizontal + bottomRightHorizontal * k, + h, + w - bottomRightHorizontal, + h); + } else { + CGPathAddLineToPoint(path, NULL, w, h); + } + + // Bottom edge → bottom-left corner + if (hasBL) { + CGPathAddLineToPoint(path, NULL, bottomLeftHorizontal, h); + CGPathAddCurveToPoint( + path, + NULL, + bottomLeftHorizontal - bottomLeftHorizontal * k, + h, + 0, + h - bottomLeftVertical + bottomLeftVertical * k, + 0, + h - bottomLeftVertical); + } else { + CGPathAddLineToPoint(path, NULL, 0, h); + } + + // Left edge → top-left corner + if (hasTL) { + CGPathAddLineToPoint(path, NULL, 0, topLeftVertical); + CGPathAddCurveToPoint( + path, + NULL, + 0, + topLeftVertical - topLeftVertical * k, + topLeftHorizontal - topLeftHorizontal * k, + 0, + topLeftHorizontal, + 0); + } + // closePath returns to the start point — (0,0) if no TL curve + + CGPathCloseSubpath(path); + + CAShapeLayer *mask = [CAShapeLayer new]; + mask.path = path; + CGPathRelease(path); + _underlayLayer.mask = mask; +} + +- (void)setUnderlayCornerRadiiWithTopLeftHorizontal:(CGFloat)topLeftHorizontal + topLeftVertical:(CGFloat)topLeftVertical + topRightHorizontal:(CGFloat)topRightHorizontal + topRightVertical:(CGFloat)topRightVertical + bottomLeftHorizontal:(CGFloat)bottomLeftHorizontal + bottomLeftVertical:(CGFloat)bottomLeftVertical + bottomRightHorizontal:(CGFloat)bottomRightHorizontal + bottomRightVertical:(CGFloat)bottomRightVertical +{ + _underlayCornerRadii[0] = topLeftHorizontal; + _underlayCornerRadii[1] = topLeftVertical; + _underlayCornerRadii[2] = topRightHorizontal; + _underlayCornerRadii[3] = topRightVertical; + _underlayCornerRadii[4] = bottomLeftHorizontal; + _underlayCornerRadii[5] = bottomLeftVertical; + _underlayCornerRadii[6] = bottomRightHorizontal; + _underlayCornerRadii[7] = bottomRightVertical; + [self applyUnderlayCornerRadii]; +} + +- (void)setUnderlayBorderInsetsWithTop:(CGFloat)top right:(CGFloat)right bottom:(CGFloat)bottom left:(CGFloat)left +{ + _underlayBorderInsets = UIEdgeInsetsMake(top, left, bottom, right); +#if !TARGET_OS_OSX + [self setNeedsLayout]; +#else + [self setNeedsLayout:YES]; +#endif +} + #if TARGET_OS_OSX - (void)mouseDown:(NSEvent *)event { @@ -367,30 +578,6 @@ - (RNGHUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event return inner; } -- (void)setBorderRadius:(CGFloat)radius -{ - if (_borderRadius == radius) { - return; - } - - _borderRadius = radius; - [self.layer setNeedsDisplay]; -} - -- (void)displayLayer:(CALayer *)layer -{ - if (CGSizeEqualToSize(layer.bounds.size, CGSizeZero)) { - return; - } - - const CGFloat radius = MAX(0, _borderRadius); - const CGSize size = self.bounds.size; - const CGFloat scaleFactor = RCTZeroIfNaN(MIN(1, size.width / (2 * radius))); - const CGFloat currentBorderRadius = radius * scaleFactor; - layer.cornerRadius = currentBorderRadius; - _underlayLayer.cornerRadius = currentBorderRadius; -} - - (NSString *)accessibilityLabel { NSString *label = super.accessibilityLabel; diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm index a87ca71a71..e840cfb296 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm @@ -7,6 +7,7 @@ #import #import #import +#import #import #import "RNGestureHandlerButton.h" @@ -56,6 +57,7 @@ - (instancetype)initWithFrame:(CGRect)frame static const auto defaultProps = std::make_shared(); _props = defaultProps; _buttonView = [[RNGestureHandlerButton alloc] initWithFrame:self.bounds]; + _buttonView.animationTarget = self; self.contentView = _buttonView; } @@ -63,26 +65,6 @@ - (instancetype)initWithFrame:(CGRect)frame return self; } -#if !TARGET_OS_OSX -- (void)willMoveToSuperview:(RNGHUIView *)newSuperview -{ - [super willMoveToSuperview:newSuperview]; - _buttonView.animationTarget = newSuperview; - if (newSuperview != nil) { - [_buttonView applyStartAnimationState]; - } -} -#else -- (void)viewWillMoveToSuperview:(RNGHUIView *)newSuperview -{ - [super viewWillMoveToSuperview:newSuperview]; - _buttonView.animationTarget = newSuperview; - if (newSuperview != nil) { - [_buttonView applyStartAnimationState]; - } -} -#endif - - (void)mountChildComponentView:(RNGHUIView *)childComponentView index:(NSInteger)index { [_buttonView mountChildComponentView:childComponentView index:index]; @@ -128,6 +110,27 @@ - (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetric [_buttonView updateLayoutMetrics:buttonMetrics oldLayoutMetrics:oldbuttonMetrics]; } +- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask +{ + [super finalizeUpdates:updateMask]; + + // Resolve per-corner border radii from props and forward to the button + // so its underlay CALayer gets the matching shape. + const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics); + [_buttonView setUnderlayCornerRadiiWithTopLeftHorizontal:borderMetrics.borderRadii.topLeft.horizontal + topLeftVertical:borderMetrics.borderRadii.topLeft.vertical + topRightHorizontal:borderMetrics.borderRadii.topRight.horizontal + topRightVertical:borderMetrics.borderRadii.topRight.vertical + bottomLeftHorizontal:borderMetrics.borderRadii.bottomLeft.horizontal + bottomLeftVertical:borderMetrics.borderRadii.bottomLeft.vertical + bottomRightHorizontal:borderMetrics.borderRadii.bottomRight.horizontal + bottomRightVertical:borderMetrics.borderRadii.bottomRight.vertical]; + [_buttonView setUnderlayBorderInsetsWithTop:borderMetrics.borderWidths.top + right:borderMetrics.borderWidths.right + bottom:borderMetrics.borderWidths.bottom + left:borderMetrics.borderWidths.left]; +} + #pragma mark - RCTComponentViewProtocol + (ComponentDescriptorProvider)componentDescriptorProvider @@ -268,9 +271,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & } [super updateProps:props oldProps:oldProps]; - if (_buttonView.animationTarget != nil) { - [_buttonView applyStartAnimationState]; - } + [_buttonView applyStartAnimationState]; } #if !TARGET_OS_OSX diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.h b/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.h deleted file mode 100644 index c94ecf946e..0000000000 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.h +++ /dev/null @@ -1,19 +0,0 @@ -#if !TARGET_OS_OSX -#import -#else -#import -#endif - -#import - -#import - -using namespace facebook::react; - -NS_ASSUME_NONNULL_BEGIN - -@interface RNGestureHandlerButtonWrapper : RCTViewComponentView - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.mm deleted file mode 100644 index 526c7743d6..0000000000 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.mm +++ /dev/null @@ -1,46 +0,0 @@ -#import "RNGestureHandlerButtonWrapper.h" -#import "RNGestureHandlerButtonWrapperComponentDescriptor.h" -#import "RNGestureHandlerModule.h" - -#import -#import - -#import -#import -#import - -@interface RNGestureHandlerButtonWrapper () -@end - -@implementation RNGestureHandlerButtonWrapper - -#if TARGET_OS_OSX -+ (BOOL)shouldBeRecycled -{ - return NO; -} -#endif - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - static const auto defaultProps = std::make_shared(); - _props = defaultProps; - } - - return self; -} - -#pragma mark - RCTComponentViewProtocol - -+ (ComponentDescriptorProvider)componentDescriptorProvider -{ - return concreteComponentDescriptorProvider(); -} - -@end - -Class RNGestureHandlerButtonWrapperCls(void) -{ - return RNGestureHandlerButtonWrapper.class; -} diff --git a/packages/react-native-gesture-handler/package.json b/packages/react-native-gesture-handler/package.json index 3d11ad594b..da5575a62a 100644 --- a/packages/react-native-gesture-handler/package.json +++ b/packages/react-native-gesture-handler/package.json @@ -129,8 +129,7 @@ "ios": { "componentProvider": { "RNGestureHandlerButton": "RNGestureHandlerButtonComponentView", - "RNGestureHandlerDetector": "RNGestureHandlerDetector", - "RNGestureHandlerButtonWrapper": "RNGestureHandlerButtonWrapper" + "RNGestureHandlerDetector": "RNGestureHandlerDetector" } } }, diff --git a/packages/react-native-gesture-handler/react-native.config.js b/packages/react-native-gesture-handler/react-native.config.js index 46e3de4682..d19502f02d 100644 --- a/packages/react-native-gesture-handler/react-native.config.js +++ b/packages/react-native-gesture-handler/react-native.config.js @@ -4,7 +4,6 @@ module.exports = { android: { componentDescriptors: [ 'RNGestureHandlerDetectorComponentDescriptor', - 'RNGestureHandlerButtonWrapperComponentDescriptor', ], cmakeListsPath: './CMakeLists.txt', }, diff --git a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/ComponentDescriptors.h b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/ComponentDescriptors.h index 5621db20e7..9ca21c682c 100644 --- a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/ComponentDescriptors.h +++ b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/ComponentDescriptors.h @@ -15,7 +15,6 @@ #include #include -#include #include namespace facebook::react { diff --git a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperComponentDescriptor.h b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperComponentDescriptor.h deleted file mode 100644 index c3323ae823..0000000000 --- a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperComponentDescriptor.h +++ /dev/null @@ -1,32 +0,0 @@ - -/** - * This code was generated by - * [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). - * - * Do not edit this file as changes may cause incorrect behavior and will be - * lost once the code is regenerated. - * - * @generated by codegen project: GenerateComponentDescriptorH.js - */ - -#pragma once - -#include - -#include "RNGestureHandlerButtonWrapperShadowNode.h" - -namespace facebook::react { - -class RNGestureHandlerButtonWrapperComponentDescriptor final - : public ConcreteComponentDescriptor< - RNGestureHandlerButtonWrapperShadowNode> { - using ConcreteComponentDescriptor::ConcreteComponentDescriptor; - void adopt(ShadowNode &shadowNode) const override { - react_native_assert( - dynamic_cast(&shadowNode)); - - ConcreteComponentDescriptor::adopt(shadowNode); - } -}; - -} // namespace facebook::react diff --git a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.cpp b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.cpp deleted file mode 100644 index 682baf51f8..0000000000 --- a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.cpp +++ /dev/null @@ -1,102 +0,0 @@ - -#include - -#include "RNGestureHandlerButtonWrapperShadowNode.h" - -namespace facebook::react { - -extern const char RNGestureHandlerButtonWrapperComponentName[] = - "RNGestureHandlerButtonWrapper"; - -void RNGestureHandlerButtonWrapperShadowNode::initialize() { - // When the button wrapper is cloned and has a child node, the child node - // should be cloned as well to ensure it is mutable. - if (!getChildren().empty()) { - prepareChildren(); - } -} - -void RNGestureHandlerButtonWrapperShadowNode::prepareChildren() { - const auto &children = getChildren(); - react_native_assert( - children.size() == 1 && - "RNGestureHandlerButtonWrapper received more than one child"); - - const auto directChild = children[0]; - react_native_assert( - directChild->getChildren().size() == 1 && - "RNGestureHandlerButtonWrapper received more than one grandchild"); - - const auto clonedChild = directChild->clone({}); - - const auto childWithProtectedAccess = - std::static_pointer_cast( - clonedChild); - childWithProtectedAccess->traits_.unset(ShadowNodeTraits::ForceFlattenView); - - replaceChild(*directChild, clonedChild); - - const auto grandChild = clonedChild->getChildren()[0]; - const auto clonedGrandChild = grandChild->clone({}); - clonedChild->replaceChild(*grandChild, clonedGrandChild); -} - -void RNGestureHandlerButtonWrapperShadowNode::appendChild( - const std::shared_ptr &child) { - YogaLayoutableShadowNode::appendChild(child); - prepareChildren(); -} - -void RNGestureHandlerButtonWrapperShadowNode::layout( - LayoutContext layoutContext) { - react_native_assert(getChildren().size() == 1); - react_native_assert(getChildren()[0]->getChildren().size() == 1); - - auto child = std::static_pointer_cast( - getChildren()[0]); - auto grandChild = std::static_pointer_cast( - child->getChildren()[0]); - - auto gradChildWithProtectedAccess = - std::static_pointer_cast( - grandChild); - - auto shouldSkipCustomLayout = - !gradChildWithProtectedAccess->yogaNode_.getHasNewLayout(); - - YogaLayoutableShadowNode::layout(layoutContext); - - child->ensureUnsealed(); - grandChild->ensureUnsealed(); - - auto mutableChild = std::const_pointer_cast(child); - auto mutableGrandChild = - std::const_pointer_cast(grandChild); - - // The grand child node did not have its layout changed, we can reuse previous - // values - if (shouldSkipCustomLayout) { - react_native_assert(previousGrandChildLayoutMetrics_.has_value()); - setLayoutMetrics(previousGrandChildLayoutMetrics_.value()); - - auto metricsNoOrigin = previousGrandChildLayoutMetrics_.value(); - metricsNoOrigin.frame.origin = Point{}; - - mutableChild->setLayoutMetrics(metricsNoOrigin); - mutableGrandChild->setLayoutMetrics(metricsNoOrigin); - return; - } - - auto metrics = grandChild->getLayoutMetrics(); - previousGrandChildLayoutMetrics_ = metrics; - - setLayoutMetrics(metrics); - - auto metricsNoOrigin = grandChild->getLayoutMetrics(); - metricsNoOrigin.frame.origin = Point{}; - - mutableChild->setLayoutMetrics(metricsNoOrigin); - mutableGrandChild->setLayoutMetrics(metricsNoOrigin); -} - -} // namespace facebook::react diff --git a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.h b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.h deleted file mode 100644 index f04021c054..0000000000 --- a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.h +++ /dev/null @@ -1,56 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "RNGestureHandlerButtonWrapperState.h" - -namespace facebook::react { - -JSI_EXPORT extern const char RNGestureHandlerButtonWrapperComponentName[]; - -/* - * `ShadowNode` for component. - */ -class RNGestureHandlerButtonWrapperShadowNode final - : public ConcreteViewShadowNode< - RNGestureHandlerButtonWrapperComponentName, - RNGestureHandlerButtonWrapperProps, - RNGestureHandlerButtonWrapperEventEmitter, - RNGestureHandlerButtonWrapperState> { - public: - RNGestureHandlerButtonWrapperShadowNode( - const ShadowNodeFragment &fragment, - const ShadowNodeFamily::Shared &family, - ShadowNodeTraits traits) - : ConcreteViewShadowNode(fragment, family, traits) { - initialize(); - } - - RNGestureHandlerButtonWrapperShadowNode( - const ShadowNode &sourceShadowNode, - const ShadowNodeFragment &fragment) - : ConcreteViewShadowNode(sourceShadowNode, fragment) { - const auto &sourceWrapperNode = - static_cast( - sourceShadowNode); - previousGrandChildLayoutMetrics_ = - sourceWrapperNode.previousGrandChildLayoutMetrics_; - - initialize(); - } - - void layout(LayoutContext layoutContext) override; - void appendChild(const std::shared_ptr &child) override; - - private: - void initialize(); - void prepareChildren(); - - std::optional previousGrandChildLayoutMetrics_; -}; - -} // namespace facebook::react diff --git a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperState.h b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperState.h deleted file mode 100644 index 7d9fa242e0..0000000000 --- a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperState.h +++ /dev/null @@ -1,32 +0,0 @@ -/** - * This code was generated by - * [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). - * - * Do not edit this file as changes may cause incorrect behavior and will be - * lost once the code is regenerated. - * - * @generated by codegen project: GenerateStateH.js - */ -#pragma once - -#ifdef ANDROID -#include -#endif - -namespace facebook::react { - -class RNGestureHandlerButtonWrapperState { - public: - RNGestureHandlerButtonWrapperState() = default; - -#ifdef ANDROID - RNGestureHandlerButtonWrapperState( - RNGestureHandlerButtonWrapperState const &previousState, - folly::dynamic data){}; - folly::dynamic getDynamic() const { - return {}; - }; -#endif -}; - -} // namespace facebook::react diff --git a/packages/react-native-gesture-handler/src/components/GestureButtons.tsx b/packages/react-native-gesture-handler/src/components/GestureButtons.tsx index 2832752605..238e2ff671 100644 --- a/packages/react-native-gesture-handler/src/components/GestureButtons.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureButtons.tsx @@ -19,12 +19,13 @@ import type { LegacyBorderlessButtonProps, LegacyRawButtonProps, } from './GestureButtonsProps'; +import type { HostComponent } from 'react-native'; /** * @deprecated use `RawButton` instead */ export const LegacyRawButton = createNativeWrapper( - GestureHandlerButton, + GestureHandlerButton as unknown as HostComponent, { shouldCancelWhenOutside: false, shouldActivateOnStart: false, diff --git a/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx b/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx index 30ea4c0404..7e13963721 100644 --- a/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx @@ -4,17 +4,10 @@ import { HostComponent, LayoutChangeEvent, StyleProp, - StyleSheet, - View, ViewProps, ViewStyle, } from 'react-native'; import RNGestureHandlerButtonNativeComponent from '../specs/RNGestureHandlerButtonNativeComponent'; -import RNGestureHandlerButtonWrapperNativeComponent from '../specs/RNGestureHandlerButtonWrapperNativeComponent'; -import { useMemo } from 'react'; - -export const ButtonComponent = - RNGestureHandlerButtonNativeComponent as HostComponent; export interface ButtonProps extends ViewProps, AccessibilityProps { children?: React.ReactNode; @@ -144,150 +137,7 @@ export interface ButtonProps extends ViewProps, AccessibilityProps { testOnly_onLongPress?: Function | null | undefined; } -export default function GestureHandlerButton({ style, ...rest }: ButtonProps) { - const flattenedStyle = useMemo(() => StyleSheet.flatten(style), [style]); - - const { - // Layout properties - display, - width, - height, - minWidth, - maxWidth, - minHeight, - maxHeight, - flex, - flexGrow, - flexShrink, - flexBasis, - flexDirection, - flexWrap, - justifyContent, - alignItems, - alignContent, - alignSelf, - aspectRatio, - gap, - rowGap, - columnGap, - margin, - marginTop, - marginBottom, - marginLeft, - marginRight, - marginVertical, - marginHorizontal, - marginStart, - marginEnd, - padding, - paddingTop, - paddingBottom, - paddingLeft, - paddingRight, - paddingVertical, - paddingHorizontal, - paddingStart, - paddingEnd, - position, - top, - right, - bottom, - left, - start, - end, - overflow, - - // Visual properties - ...restStyle - } = flattenedStyle; - - // Layout styles for ButtonComponent - const layoutStyle = useMemo( - () => ({ - display, - width, - height, - minWidth, - maxWidth, - minHeight, - maxHeight, - flex, - flexGrow, - flexShrink, - flexBasis, - flexDirection, - flexWrap, - justifyContent, - alignItems, - alignContent, - alignSelf, - aspectRatio, - gap, - rowGap, - columnGap, - margin, - marginTop, - marginBottom, - marginLeft, - marginRight, - marginVertical, - marginHorizontal, - marginStart, - marginEnd, - padding, - paddingTop, - paddingBottom, - paddingLeft, - paddingRight, - paddingVertical, - paddingHorizontal, - paddingStart, - paddingEnd, - position, - top, - right, - bottom, - left, - start, - end, - overflow, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [flattenedStyle] - ); - - const { defaultOpacity, defaultScale } = rest; - - const buttonRestingStyle = useMemo( - (): ViewStyle => ({ - opacity: defaultOpacity, - transform: - defaultScale !== undefined ? [{ scale: defaultScale }] : undefined, - }), - [defaultOpacity, defaultScale] - ); - - return ( - - - - - - ); -} +export const ButtonComponent = + RNGestureHandlerButtonNativeComponent as HostComponent; -const styles = StyleSheet.create({ - contents: { - display: 'contents', - }, - overflowHidden: { - overflow: 'hidden', - }, -}); +export default ButtonComponent; diff --git a/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts b/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts index 47d37f37cf..6ab8bcaca7 100644 --- a/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts +++ b/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts @@ -15,9 +15,6 @@ interface NativeProps extends ViewProps { rippleColor?: ColorValue; rippleRadius?: Int32; touchSoundDisabled?: WithDefault; - borderWidth?: Float; - borderColor?: ColorValue; - borderStyle?: WithDefault; pointerEvents?: WithDefault< 'box-none' | 'none' | 'box-only' | 'auto', 'auto' @@ -30,6 +27,43 @@ interface NativeProps extends ViewProps { defaultScale?: WithDefault; defaultUnderlayOpacity?: WithDefault; underlayColor?: ColorValue; + + // Border style + borderWidth?: Float; + borderColor?: ColorValue; + borderStyle?: WithDefault; + overflow?: WithDefault; + + // Border width per-edge + borderLeftWidth?: Float; + borderRightWidth?: Float; + borderTopWidth?: Float; + borderBottomWidth?: Float; + borderStartWidth?: Float; + borderEndWidth?: Float; + + // Border color per-edge + borderLeftColor?: ColorValue; + borderRightColor?: ColorValue; + borderTopColor?: ColorValue; + borderBottomColor?: ColorValue; + borderStartColor?: ColorValue; + borderEndColor?: ColorValue; + borderBlockColor?: ColorValue; + 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; } export default codegenNativeComponent('RNGestureHandlerButton'); diff --git a/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonWrapperNativeComponent.ts b/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonWrapperNativeComponent.ts deleted file mode 100644 index d84e610aaa..0000000000 --- a/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonWrapperNativeComponent.ts +++ /dev/null @@ -1,11 +0,0 @@ -import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; -import type { ViewProps } from 'react-native'; - -interface NativeProps extends ViewProps {} - -export default codegenNativeComponent( - 'RNGestureHandlerButtonWrapper', - { - interfaceOnly: true, - } -);