Skip to content

Commit 00c0749

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 a682007 commit 00c0749

4 files changed

Lines changed: 78 additions & 6 deletions

File tree

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

Lines changed: 33 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
@@ -33,6 +34,7 @@ import com.facebook.react.uimanager.drawable.MIN_INSET_BOX_SHADOW_SDK_VERSION
3334
import com.facebook.react.uimanager.drawable.MIN_OUTSET_BOX_SHADOW_SDK_VERSION
3435
import com.facebook.react.uimanager.drawable.OutlineDrawable
3536
import com.facebook.react.uimanager.drawable.OutsetBoxShadowDrawable
37+
import com.facebook.react.uimanager.drawable.RippleMaskDrawable
3638
import com.facebook.react.uimanager.style.BackgroundImageLayer
3739
import com.facebook.react.uimanager.style.BackgroundPosition
3840
import com.facebook.react.uimanager.style.BackgroundRepeat
@@ -223,6 +225,13 @@ public object BackgroundStyleApplicator {
223225
compositeBackgroundDrawable.backgroundImage?.invalidateSelf()
224226
compositeBackgroundDrawable.border?.invalidateSelf()
225227

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

454476
/**
@@ -653,6 +675,16 @@ public object BackgroundStyleApplicator {
653675
private fun getCompositeBackgroundDrawable(view: View): CompositeBackgroundDrawable? =
654676
view.background as? CompositeBackgroundDrawable
655677

678+
private fun updateRippleMaskBorderRadius(
679+
drawable: Drawable?,
680+
borderRadius: BorderRadiusStyle?,
681+
) {
682+
val mask =
683+
(drawable as? RippleDrawable)?.findDrawableByLayerId(android.R.id.mask)
684+
as? RippleMaskDrawable
685+
mask?.borderRadius = borderRadius
686+
}
687+
656688
private fun ensureBackgroundDrawable(view: View): BackgroundDrawable {
657689
val compositeBackgroundDrawable = ensureCompositeBackgroundDrawable(view)
658690
var background = compositeBackgroundDrawable.background
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.uimanager.drawable
9+
10+
import android.content.Context
11+
import android.graphics.Color
12+
import android.graphics.PixelFormat
13+
import android.graphics.drawable.DrawableWrapper
14+
import com.facebook.react.uimanager.style.BorderRadiusStyle
15+
16+
/** Mask for a bounded [android.graphics.drawable.RippleDrawable] that follows the border radius. */
17+
internal class RippleMaskDrawable private constructor(private val background: BackgroundDrawable) :
18+
DrawableWrapper(background) {
19+
20+
constructor(
21+
context: Context
22+
) : this(BackgroundDrawable(context).apply { backgroundColor = Color.WHITE })
23+
24+
var borderRadius: BorderRadiusStyle?
25+
get() = background.borderRadius
26+
set(value) {
27+
background.borderRadius = value
28+
background.invalidateSelf()
29+
}
30+
31+
@Deprecated("Deprecated in Java")
32+
override fun getOpacity(): Int =
33+
// RippleDrawable clips to an opaque mask's bounds, but renders a translucent mask into a
34+
// bitmap to clip to its shape. Only pay for the bitmap when there are corners to round.
35+
if (borderRadius?.hasRoundedBorders() == true) PixelFormat.TRANSLUCENT else PixelFormat.OPAQUE
36+
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ 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
1514
import android.graphics.drawable.Drawable
1615
import android.graphics.drawable.RippleDrawable
1716
import android.util.TypedValue
@@ -24,6 +23,7 @@ import com.facebook.react.bridge.ReadableType
2423
import com.facebook.react.common.ReactConstants
2524
import com.facebook.react.uimanager.PixelUtil
2625
import com.facebook.react.uimanager.ViewProps
26+
import com.facebook.react.uimanager.drawable.RippleMaskDrawable
2727
import kotlin.math.roundToInt
2828

2929
/**
@@ -92,7 +92,7 @@ public object ReactDrawableHelper {
9292
color = Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color))
9393
}
9494

95-
val mask = getMask(drawableDescriptionDict)
95+
val mask = getMask(context, drawableDescriptionDict)
9696
return RippleDrawable(ColorStateList(arrayOf(intArrayOf()), intArrayOf(color)), null, mask)
9797
}
9898

@@ -149,13 +149,13 @@ public object ReactDrawableHelper {
149149
)
150150
}
151151

152-
private fun getMask(drawableDescriptionDict: ReadableMap): Drawable? {
152+
private fun getMask(context: Context, drawableDescriptionDict: ReadableMap): Drawable? {
153153
if (
154154
!drawableDescriptionDict.hasKey("borderless") ||
155155
drawableDescriptionDict.isNull("borderless") ||
156156
!drawableDescriptionDict.getBoolean("borderless")
157157
) {
158-
return ColorDrawable(Color.WHITE)
158+
return RippleMaskDrawable(context)
159159
}
160160
return null
161161
}

‎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
@@ -697,7 +698,10 @@ public open class ReactViewGroup public constructor(context: Context?) :
697698

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

703707
override fun onViewAdded(child: View) {

0 commit comments

Comments
 (0)