Skip to content

Commit 8ea2c7a

Browse files
jasozhfacebook-github-bot
authored andcommitted
Clip ripple masks to match border radius on Android (#58945)
Summary: Fixes [#34553](#34553), where the ripple effect is a square even if the underlying Pressable is rounded. ### Root Cause For bounded ripples, ReactDrawableHelper creates a RippleDrawable with a ColorDrawable mask. This mask is rectangular and remains that way when added to CompositeBackgroundDrawable as a feedback layer, even if a border radius is configured. ### This PR Modify ReactDrawableHelper to replace the RippleDrawable's rectangular ColorDrawable mask with a BackgroundDrawable. If border radius is set, `getOpacity()` returns `PixelFormat.TRANSLUCENT` which makes RippleDrawable draw the mask into an alpha-only bitmap so the ripple respects the border radius. Otherwise, it returns `PixelFormat.OPAQUE` to avoid the cost of creating the bitmap and match the original ColorDrawable behavior. Modify BackgroundStyleApplicator to update the ripple mask's border radius during `setFeedbackUnderlay()`, `setFeedbackOverlay()`, and `setBorderRadius()`. Also make `ReactViewGroup.applyNativeForeground()` call `setFeedbackOverlay()` so that foreground ripples are properly clipped. Changelog: [Android][Fixed] - Fix Pressable ripple leaking past rounded corners Differential Revision: D123816883
1 parent 1324e10 commit 8ea2c7a

4 files changed

Lines changed: 58 additions & 7 deletions

File tree

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import android.graphics.PorterDuffXfermode
1616
import android.graphics.Rect
1717
import android.graphics.RectF
1818
import android.graphics.drawable.Drawable
19+
import android.graphics.drawable.RippleDrawable
1920
import android.os.Build
2021
import android.view.View
2122
import android.widget.ImageView
@@ -223,6 +224,13 @@ public object BackgroundStyleApplicator {
223224
compositeBackgroundDrawable.backgroundImage?.invalidateSelf()
224225
compositeBackgroundDrawable.border?.invalidateSelf()
225226

227+
// Update feedback underlay and overlay
228+
updateRippleMaskBorderRadius(
229+
compositeBackgroundDrawable.feedbackUnderlay,
230+
compositeBackgroundDrawable.borderRadius,
231+
)
232+
updateRippleMaskBorderRadius(view.foreground, compositeBackgroundDrawable.borderRadius)
233+
226234
if (Build.VERSION.SDK_INT >= MIN_OUTSET_BOX_SHADOW_SDK_VERSION) {
227235
for (shadow in
228236
compositeBackgroundDrawable.outerShadows.filterIsInstance<OutsetBoxShadowDrawable>()) {
@@ -448,7 +456,20 @@ public object BackgroundStyleApplicator {
448456
*/
449457
@JvmStatic
450458
public fun setFeedbackUnderlay(view: View, drawable: Drawable?) {
451-
view.background = ensureCompositeBackgroundDrawable(view).withNewFeedbackUnderlay(drawable)
459+
val compositeBackgroundDrawable = ensureCompositeBackgroundDrawable(view)
460+
updateRippleMaskBorderRadius(drawable, compositeBackgroundDrawable.borderRadius)
461+
view.background = compositeBackgroundDrawable.withNewFeedbackUnderlay(drawable)
462+
}
463+
464+
/**
465+
* Sets a feedback overlay drawable for the view.
466+
*
467+
* @param view The view to apply the feedback overlay to
468+
* @param drawable The drawable to use as feedback overlay, or null to remove
469+
*/
470+
internal fun setFeedbackOverlay(view: View, drawable: Drawable?) {
471+
updateRippleMaskBorderRadius(drawable, getCompositeBackgroundDrawable(view)?.borderRadius)
472+
view.foreground = drawable
452473
}
453474

454475
/**
@@ -653,6 +674,19 @@ public object BackgroundStyleApplicator {
653674
private fun getCompositeBackgroundDrawable(view: View): CompositeBackgroundDrawable? =
654675
view.background as? CompositeBackgroundDrawable
655676

677+
private fun updateRippleMaskBorderRadius(
678+
drawable: Drawable?,
679+
borderRadius: BorderRadiusStyle?,
680+
) {
681+
val mask =
682+
(drawable as? RippleDrawable)?.findDrawableByLayerId(android.R.id.mask)
683+
as? BackgroundDrawable
684+
if (mask != null) {
685+
mask.borderRadius = borderRadius
686+
mask.invalidateSelf()
687+
}
688+
}
689+
656690
private fun ensureBackgroundDrawable(view: View): BackgroundDrawable {
657691
val compositeBackgroundDrawable = ensureCompositeBackgroundDrawable(view)
658692
var background = compositeBackgroundDrawable.background

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundDrawable.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import com.facebook.react.uimanager.style.BorderRadiusStyle
2424
import com.facebook.react.uimanager.style.ComputedBorderRadius
2525
import kotlin.math.roundToInt
2626

27-
internal class BackgroundDrawable(
27+
internal open class BackgroundDrawable(
2828
private val context: Context,
2929
/*
3030
* We assume borderRadius & borderInsets to be shared across multiple drawables

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.kt‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import android.annotation.SuppressLint
1111
import android.content.Context
1212
import android.content.res.ColorStateList
1313
import android.graphics.Color
14-
import android.graphics.drawable.ColorDrawable
14+
import android.graphics.PixelFormat
1515
import android.graphics.drawable.Drawable
1616
import android.graphics.drawable.RippleDrawable
1717
import android.util.TypedValue
@@ -24,6 +24,7 @@ import com.facebook.react.bridge.ReadableType
2424
import com.facebook.react.common.ReactConstants
2525
import com.facebook.react.uimanager.PixelUtil
2626
import com.facebook.react.uimanager.ViewProps
27+
import com.facebook.react.uimanager.drawable.BackgroundDrawable
2728
import kotlin.math.roundToInt
2829

2930
/**
@@ -92,7 +93,7 @@ public object ReactDrawableHelper {
9293
color = Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color))
9394
}
9495

95-
val mask = getMask(drawableDescriptionDict)
96+
val mask = getMask(context, drawableDescriptionDict)
9697
return RippleDrawable(ColorStateList(arrayOf(intArrayOf()), intArrayOf(color)), null, mask)
9798
}
9899

@@ -149,14 +150,26 @@ public object ReactDrawableHelper {
149150
)
150151
}
151152

152-
private fun getMask(drawableDescriptionDict: ReadableMap): Drawable? {
153+
private fun getMask(context: Context, drawableDescriptionDict: ReadableMap): Drawable? {
153154
if (
154155
!drawableDescriptionDict.hasKey("borderless") ||
155156
drawableDescriptionDict.isNull("borderless") ||
156157
!drawableDescriptionDict.getBoolean("borderless")
157158
) {
158-
return ColorDrawable(Color.WHITE)
159+
return RippleMaskDrawable(context)
159160
}
160161
return null
161162
}
162163
}
164+
165+
private class RippleMaskDrawable(context: Context) : BackgroundDrawable(context) {
166+
init {
167+
backgroundColor = Color.WHITE
168+
}
169+
170+
@Deprecated("Deprecated in Java")
171+
override fun getOpacity(): Int =
172+
// RippleDrawable clips to an opaque mask's bounds, but renders a translucent mask into a
173+
// bitmap to clip to its shape. Only pay for the bitmap when there are corners to round.
174+
if (borderRadius?.hasRoundedBorders() == true) PixelFormat.TRANSLUCENT else PixelFormat.OPAQUE
175+
}

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderColor
4545
import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderRadius
4646
import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderStyle
4747
import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderWidth
48+
import com.facebook.react.uimanager.BackgroundStyleApplicator.setFeedbackOverlay
4849
import com.facebook.react.uimanager.BackgroundStyleApplicator.setFeedbackUnderlay
4950
import com.facebook.react.uimanager.BlendModeHelper.needsIsolatedLayer
5051
import com.facebook.react.uimanager.HasElevatedDescendantCache
@@ -696,7 +697,10 @@ public open class ReactViewGroup public constructor(context: Context?) :
696697

697698
internal fun applyNativeForeground(map: ReadableMap?) {
698699
nativeForegroundMap = map
699-
foreground = map?.let { ReactDrawableHelper.createDrawableFromJSDescription(context, it) }
700+
setFeedbackOverlay(
701+
this,
702+
map?.let { ReactDrawableHelper.createDrawableFromJSDescription(context, it) },
703+
)
700704
}
701705

702706
override fun onViewAdded(child: View) {

0 commit comments

Comments
 (0)