Skip to content

Commit fdd6ca5

Browse files
Abbondanzometa-codesync[bot]
authored andcommitted
Fix nougat blank rendering with antialiased border radius clipping (facebook#56336)
Summary: Pull Request resolved: facebook#56336 Moves `clipRect` inside the outer `saveLayer` in `clipWithAntiAliasing()` to fix blank rendering on API 24 (Nougat) devices. facebook#55762 added `canvas.withClip()` to fix black pixels on partially off-screen views. However, it wrapped the entire compositing chain in an extra `save()`/`clipRect`/`restore()`, creating a triple-nested save/restore stack (`save` -> `saveLayer` -> `saveLayer(DST_IN)`) that breaks Porter-Duff compositing on API 24's HWUI renderer. Moving `clipRect` inside the `saveLayer` reduces nesting from 3 to 2 levels while preserving the black-pixel mitigation. The `saveLayer` already saves and restores clip state, so a separate `save()`/`restore()` wrapper is unnecessary. Changelog: [Android][Fixed] - Fix image content disappearing on API 24 (Nougat) when antialiased border radius clipping is applied Reviewed By: NickGerleman Differential Revision: D99677658 fbshipit-source-id: 55a9c84655cc08b05b95c29725492b9b8f759331
1 parent a937ad4 commit fdd6ca5

File tree

1 file changed

+41
-41
lines changed

1 file changed

+41
-41
lines changed

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

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import android.os.Build
2020
import android.view.View
2121
import android.widget.ImageView
2222
import androidx.annotation.ColorInt
23-
import androidx.core.graphics.withClip
2423
import com.facebook.react.bridge.ReadableArray
2524
import com.facebook.react.common.annotations.UnstableReactNativeAPI
2625
import com.facebook.react.uimanager.PixelUtil.dpToPx
@@ -573,49 +572,50 @@ public object BackgroundStyleApplicator {
573572
paddingBoxPath: Path,
574573
drawContent: () -> Unit,
575574
) {
576-
// Clip to the view's own bounds before saveLayer. On API <= 28 hardware-accelerated canvases,
575+
// Save the layer for Porter-Duff compositing
576+
val saveCount = canvas.saveLayer(0f, 0f, view.width.toFloat(), view.height.toFloat(), null)
577+
578+
// Clip to the view's own bounds inside the layer. On API <= 28 hardware-accelerated canvases,
577579
// the window boundary is tracked by the GPU scissor but not reflected in the canvas clip stack.
578580
// Without an explicit software clip, saveLayer may allocate a buffer with uninitialized pixels
579-
// beyond the GPU scissor. Adding clipRect in the view's local coordinate space forces HWUI to
580-
// include it in the clip stack, ensuring saveLayer properly constrains its buffer. This clip is
581-
// stable across parent transform animations since it's in the view's own coordinate space.
582-
canvas.withClip(0, 0, view.width, view.height) {
583-
// Save the layer for Porter-Duff compositing
584-
val saveCount = canvas.saveLayer(0f, 0f, view.width.toFloat(), view.height.toFloat(), null)
585-
586-
// Draw the content first
587-
drawContent()
588-
589-
val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG)
590-
maskPaint.style = Paint.Style.FILL
591-
592-
// Transparent pixels with INVERSE_WINDING only works on API 28
593-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
594-
maskPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
595-
maskPaint.color = Color.TRANSPARENT
596-
paddingBoxPath.setFillType(Path.FillType.INVERSE_WINDING)
597-
canvas.drawPath(paddingBoxPath, maskPaint)
598-
} else {
599-
// API < 28: Use a nested saveLayer with DST_IN compositing to mask content to the
600-
// padding box path. EVEN_ODD fill + DST_OUT has rendering bugs on API 24's hardware
601-
// renderer, so we avoid that technique. Instead, draw the mask shape into a separate
602-
// layer; when restored with DST_IN, content is preserved only where the mask is opaque.
603-
val dstInPaint = Paint()
604-
dstInPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
605-
val maskSave =
606-
canvas.saveLayer(0f, 0f, view.width.toFloat(), view.height.toFloat(), dstInPaint)
607-
// Clear the layer to ensure it starts fully transparent. On API 24, saveLayer may not
608-
// initialize the buffer to transparent, causing DST_IN to see non-zero alpha everywhere.
609-
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
610-
maskPaint.xfermode = null
611-
maskPaint.color = Color.BLACK
612-
canvas.drawPath(paddingBoxPath, maskPaint)
613-
canvas.restoreToCount(maskSave)
614-
}
615-
616-
// Restore the layer
617-
canvas.restoreToCount(saveCount)
581+
// beyond the GPU scissor. Adding clipRect inside the layer (rather than wrapping it with
582+
// canvas.withClip) avoids an extra save/restore nesting level that breaks Porter-Duff
583+
// compositing on API 24's HWUI renderer. The saveLayer already saves and restores the clip
584+
// state, so a separate save/restore wrapper is unnecessary.
585+
canvas.clipRect(0, 0, view.width, view.height)
586+
587+
// Draw the content first
588+
drawContent()
589+
590+
val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG)
591+
maskPaint.style = Paint.Style.FILL
592+
593+
// Transparent pixels with INVERSE_WINDING only works on API 28
594+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
595+
maskPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
596+
maskPaint.color = Color.TRANSPARENT
597+
paddingBoxPath.setFillType(Path.FillType.INVERSE_WINDING)
598+
canvas.drawPath(paddingBoxPath, maskPaint)
599+
} else {
600+
// API < 28: Use a nested saveLayer with DST_IN compositing to mask content to the
601+
// padding box path. EVEN_ODD fill + DST_OUT has rendering bugs on API 24's hardware
602+
// renderer, so we avoid that technique. Instead, draw the mask shape into a separate
603+
// layer; when restored with DST_IN, content is preserved only where the mask is opaque.
604+
val dstInPaint = Paint()
605+
dstInPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
606+
val maskSave =
607+
canvas.saveLayer(0f, 0f, view.width.toFloat(), view.height.toFloat(), dstInPaint)
608+
// Clear the layer to ensure it starts fully transparent. On API 24, saveLayer may not
609+
// initialize the buffer to transparent, causing DST_IN to see non-zero alpha everywhere.
610+
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
611+
maskPaint.xfermode = null
612+
maskPaint.color = Color.BLACK
613+
canvas.drawPath(paddingBoxPath, maskPaint)
614+
canvas.restoreToCount(maskSave)
618615
}
616+
617+
// Restore the layer
618+
canvas.restoreToCount(saveCount)
619619
}
620620

621621
/**

0 commit comments

Comments
 (0)