diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt index c7d94a770c39..2ac90a6f304b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt @@ -16,6 +16,7 @@ import android.graphics.PorterDuffXfermode import android.graphics.Rect import android.graphics.RectF import android.graphics.drawable.Drawable +import android.graphics.drawable.RippleDrawable import android.os.Build import android.view.View import android.widget.ImageView @@ -223,6 +224,13 @@ public object BackgroundStyleApplicator { compositeBackgroundDrawable.backgroundImage?.invalidateSelf() compositeBackgroundDrawable.border?.invalidateSelf() + // Update feedback underlay and overlay + updateRippleMaskBorderRadius( + compositeBackgroundDrawable.feedbackUnderlay, + compositeBackgroundDrawable.borderRadius, + ) + updateRippleMaskBorderRadius(view.foreground, compositeBackgroundDrawable.borderRadius) + if (Build.VERSION.SDK_INT >= MIN_OUTSET_BOX_SHADOW_SDK_VERSION) { for (shadow in compositeBackgroundDrawable.outerShadows.filterIsInstance()) { @@ -448,7 +456,20 @@ public object BackgroundStyleApplicator { */ @JvmStatic public fun setFeedbackUnderlay(view: View, drawable: Drawable?) { - view.background = ensureCompositeBackgroundDrawable(view).withNewFeedbackUnderlay(drawable) + val compositeBackgroundDrawable = ensureCompositeBackgroundDrawable(view) + updateRippleMaskBorderRadius(drawable, compositeBackgroundDrawable.borderRadius) + view.background = compositeBackgroundDrawable.withNewFeedbackUnderlay(drawable) + } + + /** + * Sets a feedback overlay drawable for the view. + * + * @param view The view to apply the feedback overlay to + * @param drawable The drawable to use as feedback overlay, or null to remove + */ + internal fun setFeedbackOverlay(view: View, drawable: Drawable?) { + updateRippleMaskBorderRadius(drawable, getCompositeBackgroundDrawable(view)?.borderRadius) + view.foreground = drawable } /** @@ -653,6 +674,19 @@ public object BackgroundStyleApplicator { private fun getCompositeBackgroundDrawable(view: View): CompositeBackgroundDrawable? = view.background as? CompositeBackgroundDrawable + private fun updateRippleMaskBorderRadius( + drawable: Drawable?, + borderRadius: BorderRadiusStyle?, + ) { + val mask = + (drawable as? RippleDrawable)?.findDrawableByLayerId(android.R.id.mask) + as? BackgroundDrawable + if (mask != null) { + mask.borderRadius = borderRadius + mask.invalidateSelf() + } + } + private fun ensureBackgroundDrawable(view: View): BackgroundDrawable { val compositeBackgroundDrawable = ensureCompositeBackgroundDrawable(view) var background = compositeBackgroundDrawable.background diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundDrawable.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundDrawable.kt index d5a2c2d342aa..eaa20b05b563 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundDrawable.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundDrawable.kt @@ -24,7 +24,7 @@ import com.facebook.react.uimanager.style.BorderRadiusStyle import com.facebook.react.uimanager.style.ComputedBorderRadius import kotlin.math.roundToInt -internal class BackgroundDrawable( +internal open class BackgroundDrawable( private val context: Context, /* * We assume borderRadius & borderInsets to be shared across multiple drawables diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.kt index b603d3d469e9..8e1710c12366 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.kt @@ -11,7 +11,7 @@ import android.annotation.SuppressLint import android.content.Context import android.content.res.ColorStateList import android.graphics.Color -import android.graphics.drawable.ColorDrawable +import android.graphics.PixelFormat import android.graphics.drawable.Drawable import android.graphics.drawable.RippleDrawable import android.util.TypedValue @@ -24,6 +24,7 @@ import com.facebook.react.bridge.ReadableType import com.facebook.react.common.ReactConstants import com.facebook.react.uimanager.PixelUtil import com.facebook.react.uimanager.ViewProps +import com.facebook.react.uimanager.drawable.BackgroundDrawable import kotlin.math.roundToInt /** @@ -92,7 +93,7 @@ public object ReactDrawableHelper { color = Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color)) } - val mask = getMask(drawableDescriptionDict) + val mask = getMask(context, drawableDescriptionDict) return RippleDrawable(ColorStateList(arrayOf(intArrayOf()), intArrayOf(color)), null, mask) } @@ -149,14 +150,26 @@ public object ReactDrawableHelper { ) } - private fun getMask(drawableDescriptionDict: ReadableMap): Drawable? { + private fun getMask(context: Context, drawableDescriptionDict: ReadableMap): Drawable? { if ( !drawableDescriptionDict.hasKey("borderless") || drawableDescriptionDict.isNull("borderless") || !drawableDescriptionDict.getBoolean("borderless") ) { - return ColorDrawable(Color.WHITE) + return RippleMaskDrawable(context) } return null } } + +private class RippleMaskDrawable(context: Context) : BackgroundDrawable(context) { + init { + backgroundColor = Color.WHITE + } + + @Deprecated("Deprecated in Java") + override fun getOpacity(): Int = + // RippleDrawable clips to an opaque mask's bounds, but renders a translucent mask into a + // bitmap to clip to its shape. Only pay for the bitmap when there are corners to round. + if (borderRadius?.hasRoundedBorders() == true) PixelFormat.TRANSLUCENT else PixelFormat.OPAQUE +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt index 363f1336d000..0330ed628097 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt @@ -45,6 +45,7 @@ import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderColor import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderRadius import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderStyle import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderWidth +import com.facebook.react.uimanager.BackgroundStyleApplicator.setFeedbackOverlay import com.facebook.react.uimanager.BackgroundStyleApplicator.setFeedbackUnderlay import com.facebook.react.uimanager.BlendModeHelper.needsIsolatedLayer import com.facebook.react.uimanager.HasElevatedDescendantCache @@ -696,7 +697,10 @@ public open class ReactViewGroup public constructor(context: Context?) : internal fun applyNativeForeground(map: ReadableMap?) { nativeForegroundMap = map - foreground = map?.let { ReactDrawableHelper.createDrawableFromJSDescription(context, it) } + setFeedbackOverlay( + this, + map?.let { ReactDrawableHelper.createDrawableFromJSDescription(context, it) }, + ) } override fun onViewAdded(child: View) { diff --git a/packages/rn-tester/js/examples/Pressable/PressableExample.js b/packages/rn-tester/js/examples/Pressable/PressableExample.js index fc7ba7bb8005..46867959fd8c 100644 --- a/packages/rn-tester/js/examples/Pressable/PressableExample.js +++ b/packages/rn-tester/js/examples/Pressable/PressableExample.js @@ -312,6 +312,23 @@ function PressableDisabled() { ); } +function RoundedPressableExample() { + return ( + + + Background Ripple + + + Foreground Ripple + + + ); +} + const styles = StyleSheet.create({ row: { justifyContent: 'center', @@ -397,6 +414,12 @@ const styles = StyleSheet.create({ height: 100, width: 100, }, + roundedPressable: { + backgroundColor: '#eee', + borderRadius: 100, + padding: 16, + marginTop: 10, + }, }); const examples = [ @@ -714,6 +737,15 @@ const examples = [ ); }, }, + { + title: 'Rounded Pressable with Ripple', + description: + 'Ripple should clip to the border radius of the Pressable' as string, + platform: 'android', + render: function (): React.Node { + return ; + }, + }, ...PressableExampleFbInternal.examples, ];