Skip to content
Closed
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
1 change: 1 addition & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -3228,6 +3228,7 @@ public abstract interface class com/facebook/react/turbomodule/core/interfaces/T
public final class com/facebook/react/uimanager/BackgroundStyleApplicator {
public static final field INSTANCE Lcom/facebook/react/uimanager/BackgroundStyleApplicator;
public static final fun clipToPaddingBox (Landroid/view/View;Landroid/graphics/Canvas;)V
public static final fun clipToPaddingBoxWithAntiAliasing (Landroid/view/View;Landroid/graphics/Canvas;Lkotlin/jvm/functions/Function0;)V
public static final fun getBackgroundColor (Landroid/view/View;)Ljava/lang/Integer;
public static final fun getBorderColor (Landroid/view/View;Lcom/facebook/react/uimanager/style/LogicalEdge;)Ljava/lang/Integer;
public static final fun getBorderRadius (Landroid/view/View;Lcom/facebook/react/uimanager/style/BorderRadiusProp;)Lcom/facebook/react/uimanager/LengthPercentage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<10999fe360a25451c316eed2d27b6d92>>
* @generated SignedSource<<b1469e448ca6f773a3095ec2cbf4bc00>>
*/

/**
Expand Down Expand Up @@ -114,6 +114,12 @@ public object ReactNativeFeatureFlags {
@JvmStatic
public fun enableAccumulatedUpdatesInRawPropsAndroid(): Boolean = accessor.enableAccumulatedUpdatesInRawPropsAndroid()

/**
* Enable antialiased border radius clipping for Android API 28 and below using manual masking with Porter-Duff compositing
*/
@JvmStatic
public fun enableAndroidAntialiasedBorderRadiusClipping(): Boolean = accessor.enableAndroidAntialiasedBorderRadiusClipping()

/**
* Enables linear text rendering on Android wherever subpixel text rendering is enabled
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<b935cd2546fdba877e317aea30fceaf9>>
* @generated SignedSource<<b2c2e874b05283e0ebd62899f7c417d8>>
*/

/**
Expand Down Expand Up @@ -34,6 +34,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
private var disableViewPreallocationAndroidCache: Boolean? = null
private var enableAccessibilityOrderCache: Boolean? = null
private var enableAccumulatedUpdatesInRawPropsAndroidCache: Boolean? = null
private var enableAndroidAntialiasedBorderRadiusClippingCache: Boolean? = null
private var enableAndroidLinearTextCache: Boolean? = null
private var enableAndroidTextMeasurementOptimizationsCache: Boolean? = null
private var enableBridgelessArchitectureCache: Boolean? = null
Expand Down Expand Up @@ -235,6 +236,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
return cached
}

override fun enableAndroidAntialiasedBorderRadiusClipping(): Boolean {
var cached = enableAndroidAntialiasedBorderRadiusClippingCache
if (cached == null) {
cached = ReactNativeFeatureFlagsCxxInterop.enableAndroidAntialiasedBorderRadiusClipping()
enableAndroidAntialiasedBorderRadiusClippingCache = cached
}
return cached
}

override fun enableAndroidLinearText(): Boolean {
var cached = enableAndroidLinearTextCache
if (cached == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<b7a9d14c50bae9afa15b3ead8308fc9b>>
* @generated SignedSource<<ccb22ddcd1a76b7c52cf0f1b23e6152b>>
*/

/**
Expand Down Expand Up @@ -56,6 +56,8 @@ public object ReactNativeFeatureFlagsCxxInterop {

@DoNotStrip @JvmStatic public external fun enableAccumulatedUpdatesInRawPropsAndroid(): Boolean

@DoNotStrip @JvmStatic public external fun enableAndroidAntialiasedBorderRadiusClipping(): Boolean

@DoNotStrip @JvmStatic public external fun enableAndroidLinearText(): Boolean

@DoNotStrip @JvmStatic public external fun enableAndroidTextMeasurementOptimizations(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<4b24bc3165b6ab1583efc8e1a22444ed>>
* @generated SignedSource<<30ca2685ceb6f2733531f5e7fce4416d>>
*/

/**
Expand Down Expand Up @@ -51,6 +51,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi

override fun enableAccumulatedUpdatesInRawPropsAndroid(): Boolean = false

override fun enableAndroidAntialiasedBorderRadiusClipping(): Boolean = false

override fun enableAndroidLinearText(): Boolean = false

override fun enableAndroidTextMeasurementOptimizations(): Boolean = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<bd86cec4dcf659b9586aeee1c141963c>>
* @generated SignedSource<<6d1a15e64f42cc7d8869300720276215>>
*/

/**
Expand Down Expand Up @@ -38,6 +38,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
private var disableViewPreallocationAndroidCache: Boolean? = null
private var enableAccessibilityOrderCache: Boolean? = null
private var enableAccumulatedUpdatesInRawPropsAndroidCache: Boolean? = null
private var enableAndroidAntialiasedBorderRadiusClippingCache: Boolean? = null
private var enableAndroidLinearTextCache: Boolean? = null
private var enableAndroidTextMeasurementOptimizationsCache: Boolean? = null
private var enableBridgelessArchitectureCache: Boolean? = null
Expand Down Expand Up @@ -253,6 +254,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
return cached
}

override fun enableAndroidAntialiasedBorderRadiusClipping(): Boolean {
var cached = enableAndroidAntialiasedBorderRadiusClippingCache
if (cached == null) {
cached = currentProvider.enableAndroidAntialiasedBorderRadiusClipping()
accessedFeatureFlags.add("enableAndroidAntialiasedBorderRadiusClipping")
enableAndroidAntialiasedBorderRadiusClippingCache = cached
}
return cached
}

override fun enableAndroidLinearText(): Boolean {
var cached = enableAndroidLinearTextCache
if (cached == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<90fd5f8d9c5b6833c6fdd10167577bb9>>
* @generated SignedSource<<48d0d5486793b60914cfd595f0fc78d1>>
*/

/**
Expand Down Expand Up @@ -51,6 +51,8 @@ public interface ReactNativeFeatureFlagsProvider {

@DoNotStrip public fun enableAccumulatedUpdatesInRawPropsAndroid(): Boolean

@DoNotStrip public fun enableAndroidAntialiasedBorderRadiusClipping(): Boolean

@DoNotStrip public fun enableAndroidLinearText(): Boolean

@DoNotStrip public fun enableAndroidTextMeasurementOptimizations(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ package com.facebook.react.uimanager

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Drawable
Expand All @@ -19,6 +22,7 @@ import android.widget.ImageView
import androidx.annotation.ColorInt
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.common.annotations.UnstableReactNativeAPI
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
import com.facebook.react.uimanager.PixelUtil.dpToPx
import com.facebook.react.uimanager.PixelUtil.pxToDp
import com.facebook.react.uimanager.common.UIManagerType
Expand Down Expand Up @@ -481,12 +485,36 @@ public object BackgroundStyleApplicator {
*/
@JvmStatic
public fun clipToPaddingBox(view: View, canvas: Canvas) {
clipToPaddingBoxWithAntiAliasing(view, canvas, null)
}

/**
* Clips the canvas to the padding box of the view.
*
* The padding box is the area within the borders of the view, accounting for border radius if
* present.
*
* On Android 28 and below, when border radius is present, this uses an antialiased clipping
* approach with Porter-Duff compositing to avoid jagged edges. The drawContent lambda is invoked
* to draw the actual content after setting up the layer but before applying the mask.
*
* @param view The view whose padding box defines the clipping region
* @param canvas The canvas to clip
* @param drawContent Lambda that draws the content after clipping is set up
*/
@JvmStatic
public fun clipToPaddingBoxWithAntiAliasing(
view: View,
canvas: Canvas,
drawContent: (() -> Unit)?,
) {
val drawingRect = Rect()
view.getDrawingRect(drawingRect)

val composite = getCompositeBackgroundDrawable(view)
if (composite == null) {
canvas.clipRect(drawingRect)
drawContent?.invoke()
return
}

Expand All @@ -508,15 +536,69 @@ public object BackgroundStyleApplicator {
paddingBoxRect,
computedBorderInsets,
)

paddingBoxPath.offset(drawingRect.left.toFloat(), drawingRect.top.toFloat())
canvas.clipPath(paddingBoxPath)

// On Android 28 and below, use antialiased clipping with Porter-Duff compositing. On newer
// Android versions, use the standard clipPath.
if (
ReactNativeFeatureFlags.enableAndroidAntialiasedBorderRadiusClipping() &&
Build.VERSION.SDK_INT <= Build.VERSION_CODES.P &&
view.width > 0 &&
view.height > 0 &&
drawContent != null
) {
clipWithAntiAliasing(
view,
canvas,
paddingBoxPath,
drawContent,
)
} else {
canvas.clipPath(paddingBoxPath)
drawContent?.invoke()
}
} else {
paddingBoxRect.offset(drawingRect.left.toFloat(), drawingRect.top.toFloat())
canvas.clipRect(paddingBoxRect)
drawContent?.invoke()
}
}

/**
* Applies antialiased clipping using Porter-Duff compositing for Android 28 and below. This draws
* content to a layer, then applies an antialiased mask to clip it.
*/
private fun clipWithAntiAliasing(
view: View,
canvas: Canvas,
paddingBoxPath: Path,
drawContent: () -> Unit,
) {
// Save the layer for Porter-Duff compositing
val saveCount = canvas.saveLayer(0f, 0f, view.width.toFloat(), view.height.toFloat(), null)

// Draw the content first
drawContent()

// Create the antialiased mask path with Porter-Duff DST_IN to clip
val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG)
maskPaint.style = Paint.Style.FILL
maskPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)

// Transparent pixels with INVERSE_WINDING only works on API 28
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
maskPaint.color = Color.TRANSPARENT
paddingBoxPath.setFillType(Path.FillType.INVERSE_WINDING)
} else {
maskPaint.color = Color.BLACK
}

canvas.drawPath(paddingBoxPath, maskPaint)

// Restore the layer
canvas.restoreToCount(saveCount)
}

/**
* Resets the background styling of the view to its original state.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,15 +372,18 @@ public class ReactImageView(
public override fun hasOverlappingRendering(): Boolean = false

public override fun onDraw(canvas: Canvas) {
BackgroundStyleApplicator.clipToPaddingBox(this, canvas)
try {
super.onDraw(canvas)
} catch (e: RuntimeException) {
// Only provide updates if downloadListener is set (shouldNotify is true)
if (downloadListener != null) {
val eventDispatcher =
UIManagerHelper.getEventDispatcherForReactTag(context as ReactContext, id)
eventDispatcher?.dispatchEvent(createErrorEvent(UIManagerHelper.getSurfaceId(this), id, e))
BackgroundStyleApplicator.clipToPaddingBoxWithAntiAliasing(this, canvas) {
try {
super.onDraw(canvas)
} catch (e: RuntimeException) {
// Only provide updates if downloadListener is set (shouldNotify is true)
if (downloadListener != null) {
val eventDispatcher =
UIManagerHelper.getEventDispatcherForReactTag(context as ReactContext, id)
eventDispatcher?.dispatchEvent(
createErrorEvent(UIManagerHelper.getSurfaceId(this), id, e)
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<a0ef5d4a761067631023f6b5ec797cc6>>
* @generated SignedSource<<5e4e22e976ce1724191c7b7e381ea5a1>>
*/

/**
Expand Down Expand Up @@ -123,6 +123,12 @@ class ReactNativeFeatureFlagsJavaProvider
return method(javaProvider_);
}

bool enableAndroidAntialiasedBorderRadiusClipping() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("enableAndroidAntialiasedBorderRadiusClipping");
return method(javaProvider_);
}

bool enableAndroidLinearText() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("enableAndroidLinearText");
Expand Down Expand Up @@ -641,6 +647,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enableAccumulatedUpdatesInRawPropsAndro
return ReactNativeFeatureFlags::enableAccumulatedUpdatesInRawPropsAndroid();
}

bool JReactNativeFeatureFlagsCxxInterop::enableAndroidAntialiasedBorderRadiusClipping(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::enableAndroidAntialiasedBorderRadiusClipping();
}

bool JReactNativeFeatureFlagsCxxInterop::enableAndroidLinearText(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::enableAndroidLinearText();
Expand Down Expand Up @@ -1084,6 +1095,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
makeNativeMethod(
"enableAccumulatedUpdatesInRawPropsAndroid",
JReactNativeFeatureFlagsCxxInterop::enableAccumulatedUpdatesInRawPropsAndroid),
makeNativeMethod(
"enableAndroidAntialiasedBorderRadiusClipping",
JReactNativeFeatureFlagsCxxInterop::enableAndroidAntialiasedBorderRadiusClipping),
makeNativeMethod(
"enableAndroidLinearText",
JReactNativeFeatureFlagsCxxInterop::enableAndroidLinearText),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<066e98c5620ccf67d9e4ceb9d66b08f0>>
* @generated SignedSource<<1cb8ed4a72e9d2b8dd34694485704f60>>
*/

/**
Expand Down Expand Up @@ -72,6 +72,9 @@ class JReactNativeFeatureFlagsCxxInterop
static bool enableAccumulatedUpdatesInRawPropsAndroid(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);

static bool enableAndroidAntialiasedBorderRadiusClipping(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);

static bool enableAndroidLinearText(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<82169671bfbd5f83639cc50fcb710670>>
* @generated SignedSource<<9c10486574fdff78933aed6acd90533c>>
*/

/**
Expand Down Expand Up @@ -82,6 +82,10 @@ bool ReactNativeFeatureFlags::enableAccumulatedUpdatesInRawPropsAndroid() {
return getAccessor().enableAccumulatedUpdatesInRawPropsAndroid();
}

bool ReactNativeFeatureFlags::enableAndroidAntialiasedBorderRadiusClipping() {
return getAccessor().enableAndroidAntialiasedBorderRadiusClipping();
}

bool ReactNativeFeatureFlags::enableAndroidLinearText() {
return getAccessor().enableAndroidLinearText();
}
Expand Down
Loading
Loading