From db57ee75c1a354e4aad268bc27bcbd911d40bc8d Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Wed, 11 Feb 2026 13:34:33 -0800 Subject: [PATCH] Fix image disappearing on Nougat with antialiased border radius clipping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: On API 24 (Nougat), the `DST_OUT` + `EVEN_ODD` inverse path technique in `clipWithAntiAliasing()` doesn't render correctly — it erases all image content instead of just the area outside the rounded rect. The bug manifests specifically with ReactImageView (Fresco-backed) but not plain ImageView. ReactImageView's Fresco hierarchy configures `RoundingParams.RoundingMethod.BITMAP_ONLY`, which causes the drawable chain to draw through a `BitmapShader`-based paint. This shader-based drawing interacts badly with the `EVEN_ODD` fill + `DST_OUT` compositing inside a hardware-accelerated `saveLayer` on API 24's Skia renderer. The fix replaces the `EVEN_ODD` + `DST_OUT` technique with a nested `saveLayer` using `DST_IN` compositing, following the same pattern used by Facebook's Keyframes library (`AbstractLayer.applyClip`). The mask shape is drawn into a separate layer; when restored with `DST_IN`, content is preserved only where the mask is opaque. A `drawColor(CLEAR)` call after `saveLayer` ensures the layer starts fully transparent — without this, on API 24, the layer may retain parent content, causing `DST_IN` to see non-zero alpha everywhere and clip nothing. Changelog: [Android][Fixed] - Fix image content disappearing on API 24 (Nougat) when antialiased border radius clipping is applied Differential Revision: D92980234 --- .../uimanager/BackgroundStyleApplicator.kt | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt index 1837125fb2f10d..a0862a961902da 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt @@ -590,16 +590,21 @@ public object BackgroundStyleApplicator { paddingBoxPath.setFillType(Path.FillType.INVERSE_WINDING) canvas.drawPath(paddingBoxPath, maskPaint) } else { - // Create an inverse path: outer rect minus the rounded rect (using even-odd fill rule) - val inversePath = Path() - inversePath.addRect(0f, 0f, view.width.toFloat(), view.height.toFloat(), Path.Direction.CW) - inversePath.addPath(paddingBoxPath) - inversePath.setFillType(Path.FillType.EVEN_ODD) - - // Use DST_OUT to remove content where the mask is drawn (outside the rounded rect) - maskPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) + // API < 28: Use a nested saveLayer with DST_IN compositing to mask content to the + // padding box path. EVEN_ODD fill + DST_OUT has rendering bugs on API 24's hardware + // renderer, so we avoid that technique. Instead, draw the mask shape into a separate + // layer; when restored with DST_IN, content is preserved only where the mask is opaque. + val dstInPaint = Paint() + dstInPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN) + val maskSave = + canvas.saveLayer(0f, 0f, view.width.toFloat(), view.height.toFloat(), dstInPaint) + // Clear the layer to ensure it starts fully transparent. On API 24, saveLayer may not + // initialize the buffer to transparent, causing DST_IN to see non-zero alpha everywhere. + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) + maskPaint.xfermode = null maskPaint.color = Color.BLACK - canvas.drawPath(inversePath, maskPaint) + canvas.drawPath(paddingBoxPath, maskPaint) + canvas.restoreToCount(maskSave) } // Restore the layer