Skip to content

Commit f88946d

Browse files
Abbondanzometa-codesync[bot]
authored andcommitted
Fix bug where offscreen layers would erroneously draw black pixels (#55762)
Summary: Pull Request resolved: #55762 On API <= 28, `clipWithAntiAliasing` uses `saveLayer` for anti-aliased border radius clipping. When a view is partially off-screen, the GPU only renders into the visible portion of the `saveLayer` buffer, leaving off-screen pixels as opaque black. These black pixels survive Porter-Duff compositing and appear as visible artifacts. Adding `canvas.clipRect(0, 0, view.width, view.height)` before `saveLayer` forces HWUI to properly initialize the offscreen buffer relative to the GPU scissor. The clip is in local coordinates, so it stays correct across parent transform animations and scrolling offsets. Changelog: [Internal] Reviewed By: NickGerleman, jorge-cab Differential Revision: D94447724 fbshipit-source-id: cb8470a00ca489a79779fe9b8e4f200c39f83284
1 parent 05d64d9 commit f88946d

1 file changed

Lines changed: 42 additions & 33 deletions

File tree

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

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ 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
2324
import com.facebook.react.bridge.ReadableArray
2425
import com.facebook.react.common.annotations.UnstableReactNativeAPI
2526
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
@@ -574,41 +575,49 @@ public object BackgroundStyleApplicator {
574575
paddingBoxPath: Path,
575576
drawContent: () -> Unit,
576577
) {
577-
// Save the layer for Porter-Duff compositing
578-
val saveCount = canvas.saveLayer(0f, 0f, view.width.toFloat(), view.height.toFloat(), null)
579-
580-
// Draw the content first
581-
drawContent()
582-
583-
val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG)
584-
maskPaint.style = Paint.Style.FILL
578+
// Clip to the view's own bounds before saveLayer. On API <= 28 hardware-accelerated canvases,
579+
// the window boundary is tracked by the GPU scissor but not reflected in the canvas clip stack.
580+
// Without an explicit software clip, saveLayer may allocate a buffer with uninitialized pixels
581+
// beyond the GPU scissor. Adding clipRect in the view's local coordinate space forces HWUI to
582+
// include it in the clip stack, ensuring saveLayer properly constrains its buffer. This clip is
583+
// stable across parent transform animations since it's in the view's own coordinate space.
584+
canvas.withClip(0, 0, view.width, view.height) {
585+
// Save the layer for Porter-Duff compositing
586+
val saveCount = canvas.saveLayer(0f, 0f, view.width.toFloat(), view.height.toFloat(), null)
587+
588+
// Draw the content first
589+
drawContent()
590+
591+
val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG)
592+
maskPaint.style = Paint.Style.FILL
593+
594+
// Transparent pixels with INVERSE_WINDING only works on API 28
595+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
596+
maskPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
597+
maskPaint.color = Color.TRANSPARENT
598+
paddingBoxPath.setFillType(Path.FillType.INVERSE_WINDING)
599+
canvas.drawPath(paddingBoxPath, maskPaint)
600+
} else {
601+
// API < 28: Use a nested saveLayer with DST_IN compositing to mask content to the
602+
// padding box path. EVEN_ODD fill + DST_OUT has rendering bugs on API 24's hardware
603+
// renderer, so we avoid that technique. Instead, draw the mask shape into a separate
604+
// layer; when restored with DST_IN, content is preserved only where the mask is opaque.
605+
val dstInPaint = Paint()
606+
dstInPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
607+
val maskSave =
608+
canvas.saveLayer(0f, 0f, view.width.toFloat(), view.height.toFloat(), dstInPaint)
609+
// Clear the layer to ensure it starts fully transparent. On API 24, saveLayer may not
610+
// initialize the buffer to transparent, causing DST_IN to see non-zero alpha everywhere.
611+
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
612+
maskPaint.xfermode = null
613+
maskPaint.color = Color.BLACK
614+
canvas.drawPath(paddingBoxPath, maskPaint)
615+
canvas.restoreToCount(maskSave)
616+
}
585617

586-
// Transparent pixels with INVERSE_WINDING only works on API 28
587-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
588-
maskPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
589-
maskPaint.color = Color.TRANSPARENT
590-
paddingBoxPath.setFillType(Path.FillType.INVERSE_WINDING)
591-
canvas.drawPath(paddingBoxPath, maskPaint)
592-
} else {
593-
// API < 28: Use a nested saveLayer with DST_IN compositing to mask content to the
594-
// padding box path. EVEN_ODD fill + DST_OUT has rendering bugs on API 24's hardware
595-
// renderer, so we avoid that technique. Instead, draw the mask shape into a separate
596-
// layer; when restored with DST_IN, content is preserved only where the mask is opaque.
597-
val dstInPaint = Paint()
598-
dstInPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
599-
val maskSave =
600-
canvas.saveLayer(0f, 0f, view.width.toFloat(), view.height.toFloat(), dstInPaint)
601-
// Clear the layer to ensure it starts fully transparent. On API 24, saveLayer may not
602-
// initialize the buffer to transparent, causing DST_IN to see non-zero alpha everywhere.
603-
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
604-
maskPaint.xfermode = null
605-
maskPaint.color = Color.BLACK
606-
canvas.drawPath(paddingBoxPath, maskPaint)
607-
canvas.restoreToCount(maskSave)
618+
// Restore the layer
619+
canvas.restoreToCount(saveCount)
608620
}
609-
610-
// Restore the layer
611-
canvas.restoreToCount(saveCount)
612621
}
613622

614623
/**

0 commit comments

Comments
 (0)