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..d14749b85eae 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 @@ -33,6 +34,7 @@ import com.facebook.react.uimanager.drawable.MIN_INSET_BOX_SHADOW_SDK_VERSION import com.facebook.react.uimanager.drawable.MIN_OUTSET_BOX_SHADOW_SDK_VERSION import com.facebook.react.uimanager.drawable.OutlineDrawable import com.facebook.react.uimanager.drawable.OutsetBoxShadowDrawable +import com.facebook.react.uimanager.drawable.RippleMaskDrawable import com.facebook.react.uimanager.style.BackgroundImageLayer import com.facebook.react.uimanager.style.BackgroundPosition import com.facebook.react.uimanager.style.BackgroundRepeat @@ -223,6 +225,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 +457,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 +675,16 @@ 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? RippleMaskDrawable + mask?.borderRadius = borderRadius + } + 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/RippleMaskDrawable.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/RippleMaskDrawable.kt new file mode 100644 index 000000000000..98e353b267a5 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/RippleMaskDrawable.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.uimanager.drawable + +import android.content.Context +import android.graphics.Color +import android.graphics.PixelFormat +import android.graphics.drawable.DrawableWrapper +import com.facebook.react.uimanager.style.BorderRadiusStyle + +/** Mask for a bounded [android.graphics.drawable.RippleDrawable] that follows the border radius. */ +internal class RippleMaskDrawable private constructor(private val background: BackgroundDrawable) : + DrawableWrapper(background) { + + constructor( + context: Context + ) : this(BackgroundDrawable(context).apply { backgroundColor = Color.WHITE }) + + var borderRadius: BorderRadiusStyle? + get() = background.borderRadius + set(value) { + background.borderRadius = value + background.invalidateSelf() + } + + @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/ReactDrawableHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.kt index b603d3d469e9..e7ed82e26206 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,6 @@ 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.drawable.Drawable import android.graphics.drawable.RippleDrawable import android.util.TypedValue @@ -24,6 +23,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.RippleMaskDrawable import kotlin.math.roundToInt /** @@ -92,7 +92,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,13 +149,13 @@ 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 } 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 cccf2b546b71..e7331986394a 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 @@ -697,7 +698,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 e875940c9c8a..7707bc26bbbb 100644 --- a/packages/rn-tester/js/examples/Pressable/PressableExample.js +++ b/packages/rn-tester/js/examples/Pressable/PressableExample.js @@ -316,6 +316,23 @@ function PressableDisabled() { ); } +function RoundedPressableExample() { + return ( + + + Background Ripple + + + Foreground Ripple + + + ); +} + const styles = StyleSheet.create({ row: { justifyContent: 'center', @@ -401,6 +418,12 @@ const styles = StyleSheet.create({ height: 100, width: 100, }, + roundedPressable: { + backgroundColor: '#eee', + borderRadius: 100, + padding: 16, + marginTop: 10, + }, }); const examples = [ @@ -718,6 +741,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, ];