Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<OutsetBoxShadowDrawable>()) {
Expand Down Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

/**
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
32 changes: 32 additions & 0 deletions packages/rn-tester/js/examples/Pressable/PressableExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,23 @@ function PressableDisabled() {
);
}

function RoundedPressableExample() {
return (
<View>
<Pressable
android_ripple={{color: 'red', borderless: false}}
style={styles.roundedPressable}>
<Text style={{alignSelf: 'center'}}>Background Ripple</Text>
</Pressable>
<Pressable
android_ripple={{color: 'red', borderless: false, foreground: true}}
style={styles.roundedPressable}>
<Text style={{alignSelf: 'center'}}>Foreground Ripple</Text>
</Pressable>
</View>
);
}

const styles = StyleSheet.create({
row: {
justifyContent: 'center',
Expand Down Expand Up @@ -397,6 +414,12 @@ const styles = StyleSheet.create({
height: 100,
width: 100,
},
roundedPressable: {
backgroundColor: '#eee',
borderRadius: 100,
padding: 16,
marginTop: 10,
},
});

const examples = [
Expand Down Expand Up @@ -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 <RoundedPressableExample />;
},
},
...PressableExampleFbInternal.examples,
];

Expand Down
Loading