@@ -12,10 +12,17 @@ package com.facebook.react.views.view
1212import android.annotation.SuppressLint
1313import android.annotation.TargetApi
1414import android.content.Context
15+ import android.graphics.Bitmap
1516import android.graphics.BlendMode
1617import android.graphics.Canvas
1718import android.graphics.Paint
19+ import android.graphics.Path
20+ import android.graphics.PorterDuff
21+ import android.graphics.PorterDuffXfermode
22+ import android.graphics.RadialGradient
1823import android.graphics.Rect
24+ import android.graphics.RectF
25+ import android.graphics.Shader
1926import android.graphics.drawable.Drawable
2027import android.os.Build
2128import android.view.MotionEvent
@@ -67,6 +74,7 @@ import com.facebook.react.uimanager.style.LogicalEdge
6774import com.facebook.react.uimanager.style.Overflow
6875import com.facebook.react.views.view.CanvasUtil.enableZ
6976import java.util.ArrayList
77+ import java.util.Random
7078import kotlin.concurrent.Volatile
7179import kotlin.math.max
7280
@@ -156,6 +164,172 @@ public open class ReactViewGroup public constructor(context: Context?) :
156164 null
157165 private var focusOnAttach = false
158166
167+ /* *
168+ * Bitmap used to mask the view. When set, the view content will be masked using Porter-Duff
169+ * DST_IN mode. The bitmap's alpha channel determines what parts of the view are visible.
170+ */
171+ internal var maskBitmap: Bitmap ? = null
172+ set(value) {
173+ if (field != value) {
174+ field?.recycle()
175+ field = value
176+ // Disable hardware acceleration when masking (required for Porter-Duff compositing)
177+ if (value != null ) {
178+ setLayerType(View .LAYER_TYPE_SOFTWARE , null )
179+ } else {
180+ setLayerType(View .LAYER_TYPE_NONE , null )
181+ }
182+ }
183+ }
184+
185+ /* * Flag to indicate that a test mask should be generated when view gets dimensions */
186+ private var shouldGenerateTestMask = false
187+
188+ /* *
189+ * Creates a test bitmap mask with noise pattern for testing masking functionality. This generates
190+ * a random noise pattern where each pixel has a random alpha value.
191+ *
192+ * @param width Width of the mask bitmap
193+ * @param height Height of the mask bitmap
194+ * @param noiseIntensity Controls the randomness (0.0 = solid, 1.0 = full noise)
195+ * @return A bitmap with noise pattern in the alpha channel
196+ */
197+ private fun createNoiseMaskBitmap (width : Int , height : Int , noiseIntensity : Float = 0.7f): Bitmap {
198+ val bitmap = Bitmap .createBitmap(width, height, Bitmap .Config .ARGB_8888 )
199+ val pixels = IntArray (width * height)
200+ val random = Random ()
201+
202+ for (i in pixels.indices) {
203+ val alpha = (random.nextFloat() * 255 * noiseIntensity).toInt()
204+ // Create a pixel with random alpha, white color
205+ pixels[i] = (alpha shl 24 ) or 0x00FFFFFF
206+ }
207+
208+ bitmap.setPixels(pixels, 0 , width, 0 , 0 , width, height)
209+ return bitmap
210+ }
211+
212+ /* *
213+ * Creates a test bitmap mask with a custom shape (circular gradient) for testing masking
214+ * functionality. This generates a radial gradient mask from center to edges.
215+ *
216+ * @param width Width of the mask bitmap
217+ * @param height Height of the mask bitmap
218+ * @return A bitmap with circular gradient pattern in the alpha channel
219+ */
220+ private fun createCircularGradientMaskBitmap (width : Int , height : Int ): Bitmap {
221+ val bitmap = Bitmap .createBitmap(width, height, Bitmap .Config .ARGB_8888 )
222+ val canvas = Canvas (bitmap)
223+ val centerX = width / 2f
224+ val centerY = height / 2f
225+ val radius = minOf(width, height) / 2f
226+
227+ val gradient =
228+ RadialGradient (
229+ centerX,
230+ centerY,
231+ radius,
232+ intArrayOf(0xFFFFFFFF .toInt(), 0x00FFFFFF .toInt()),
233+ floatArrayOf(0f , 1f ),
234+ Shader .TileMode .CLAMP
235+ )
236+
237+ val paint = Paint (Paint .ANTI_ALIAS_FLAG ).apply { shader = gradient }
238+
239+ canvas.drawRect(0f , 0f , width.toFloat(), height.toFloat(), paint)
240+
241+ // Verify the bitmap has alpha channel
242+ println (" Created gradient mask: hasAlpha=${bitmap.hasAlpha()} , config=${bitmap.config} " )
243+
244+ return bitmap
245+ }
246+
247+ /* *
248+ * Creates a test bitmap mask with a star shape for testing masking functionality.
249+ *
250+ * @param width Width of the mask bitmap
251+ * @param height Height of the mask bitmap
252+ * @return A bitmap with star shape pattern in the alpha channel
253+ */
254+ private fun createStarMaskBitmap (width : Int , height : Int ): Bitmap {
255+ val bitmap = Bitmap .createBitmap(width, height, Bitmap .Config .ARGB_8888 )
256+ val canvas = Canvas (bitmap)
257+ val centerX = width / 2f
258+ val centerY = height / 2f
259+ val outerRadius = minOf(width, height) / 2f * 0.9f
260+ val innerRadius = outerRadius * 0.4f
261+ val numPoints = 5
262+
263+ val path = Path ()
264+ for (i in 0 until numPoints * 2 ) {
265+ val angle = (i * Math .PI / numPoints).toFloat()
266+ val radius = if (i % 2 == 0 ) outerRadius else innerRadius
267+ val x = centerX + radius * kotlin.math.cos(angle)
268+ val y = centerY + radius * kotlin.math.sin(angle)
269+
270+ if (i == 0 ) {
271+ path.moveTo(x, y)
272+ } else {
273+ path.lineTo(x, y)
274+ }
275+ }
276+ path.close()
277+
278+ val paint =
279+ Paint (Paint .ANTI_ALIAS_FLAG ).apply {
280+ color = 0xFFFFFFFF .toInt()
281+ style = Paint .Style .FILL
282+ }
283+
284+ canvas.drawPath(path, paint)
285+ return bitmap
286+ }
287+
288+ /* *
289+ * Generates a test mask bitmap for testing purposes. Currently uses noise pattern, but can be
290+ * switched to other patterns.
291+ */
292+ internal fun generateTestMask () {
293+ if (width > 0 && height > 0 ) {
294+ // Use circular gradient mask for more visible testing - can switch to createNoiseMaskBitmap
295+ // or
296+ // createStarMaskBitmap
297+ maskBitmap = createStarMaskBitmap(width, height)
298+ shouldGenerateTestMask = false
299+ println (
300+ " Mask generated: ${width} x${height} , bitmap: ${maskBitmap?.width} x${maskBitmap?.height} "
301+ )
302+ invalidate()
303+ } else {
304+ // View doesn't have dimensions yet, mark to generate later
305+ shouldGenerateTestMask = true
306+ println (" Mask generation deferred - view size: ${width} x${height} " )
307+ }
308+ }
309+
310+ /* *
311+ * Creates a bitmap mask from a gradient shader. The shader's colors are converted to alpha values
312+ * for masking.
313+ *
314+ * @param width Width of the mask bitmap
315+ * @param height Height of the mask bitmap
316+ * @param shader The gradient shader to use for masking
317+ * @return A bitmap with gradient pattern in the alpha channel
318+ */
319+ internal fun createGradientMaskBitmap (width : Int , height : Int , shader : Shader ): Bitmap {
320+ val bitmap = Bitmap .createBitmap(width, height, Bitmap .Config .ARGB_8888 )
321+ val canvas = Canvas (bitmap)
322+
323+ val paint = Paint (Paint .ANTI_ALIAS_FLAG ).apply { this .shader = shader }
324+
325+ // Draw the gradient - the alpha channel will be used for masking
326+ canvas.drawRect(0f , 0f , width.toFloat(), height.toFloat(), paint)
327+
328+ println (" Created gradient mask from shader: ${width} x${height} , hasAlpha=${bitmap.hasAlpha()} " )
329+
330+ return bitmap
331+ }
332+
159333 init {
160334 initView()
161335 }
@@ -213,6 +387,9 @@ public open class ReactViewGroup public constructor(context: Context?) :
213387 // Reset background, borders
214388 updateBackgroundDrawable(null )
215389
390+ // Reset mask bitmap (this also resets layer type to LAYER_TYPE_NONE)
391+ maskBitmap = null
392+
216393 resetPointerEvents()
217394
218395 // In case a focus was attempted but the view never attached, reset to false
@@ -584,6 +761,19 @@ public open class ReactViewGroup public constructor(context: Context?) :
584761 if (_removeClippedSubviews ) {
585762 updateClippingRect()
586763 }
764+ // Generate test mask if it was requested before we had dimensions
765+ if (shouldGenerateTestMask && w > 0 && h > 0 ) {
766+ generateTestMask()
767+ }
768+ // Generate gradient mask if one was stored before we had dimensions
769+ val gradientLayer =
770+ getTag(R .id.mask_gradient_layer) as ?
771+ com.facebook.react.uimanager.style.BackgroundImageLayer
772+ if (gradientLayer != null && w > 0 && h > 0 ) {
773+ val shader = gradientLayer.getShader(w.toFloat(), h.toFloat())
774+ maskBitmap = createGradientMaskBitmap(w, h, shader)
775+ setTag(R .id.mask_gradient_layer, null )
776+ }
587777 }
588778
589779 override fun onAttachedToWindow () {
@@ -893,35 +1083,89 @@ public open class ReactViewGroup public constructor(context: Context?) :
8931083 }
8941084
8951085 override fun draw (canvas : Canvas ) {
896- if (
897- Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q &&
898- getUIManagerType(this ) == UIManagerType .FABRIC &&
899- needsIsolatedLayer(this )
900- ) {
901- // Check if the view is a stacking context and has children, if it does, do the rendering
902- // offscreen and then composite back. This follows the idea of group isolation on blending
903- // https://www.w3.org/TR/compositing-1/#isolationblending
904-
905- val overflowInset = overflowInset
906- canvas.saveLayer(
907- overflowInset.left.toFloat(),
908- overflowInset.top.toFloat(),
909- (width + - overflowInset.right).toFloat(),
910- (height + - overflowInset.bottom).toFloat(),
911- null ,
912- )
913- super .draw(canvas)
914- canvas.restore()
1086+ val bitmap = maskBitmap
1087+ if (bitmap != null && width > 0 && height > 0 ) {
1088+ // Save layer for Porter-Duff compositing to mask everything (background + children)
1089+ val bounds = RectF (0f , 0f , width.toFloat(), height.toFloat())
1090+ val saveCount = canvas.saveLayer(bounds, null )
1091+
1092+ // Draw everything first: background (via super.draw) and children (via dispatchDraw)
1093+ // super.draw() will:
1094+ // 1. Draw background
1095+ // 2. Call dispatchDraw() which draws children
1096+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q &&
1097+ getUIManagerType(this ) == UIManagerType .FABRIC &&
1098+ needsIsolatedLayer(this )
1099+ ) {
1100+ val overflowInset = overflowInset
1101+ canvas.saveLayer(
1102+ overflowInset.left.toFloat(),
1103+ overflowInset.top.toFloat(),
1104+ (width + - overflowInset.right).toFloat(),
1105+ (height + - overflowInset.bottom).toFloat(),
1106+ null ,
1107+ )
1108+ // Call parent's draw which will draw background and call dispatchDraw for children
1109+ super .draw(canvas)
1110+ canvas.restore()
1111+ } else {
1112+ // Call parent's draw which will draw background and call dispatchDraw for children
1113+ super .draw(canvas)
1114+ }
1115+
1116+ // Now apply the mask to everything that was drawn (background + children)
1117+ val maskPaint = Paint (Paint .ANTI_ALIAS_FLAG ).apply {
1118+ xfermode = PorterDuffXfermode (PorterDuff .Mode .DST_IN )
1119+ isFilterBitmap = true
1120+ }
1121+
1122+ val srcRect = Rect (0 , 0 , bitmap.width, bitmap.height)
1123+ val dstRect = RectF (0f , 0f , width.toFloat(), height.toFloat())
1124+ canvas.drawBitmap(bitmap, srcRect, dstRect, maskPaint)
1125+
1126+ // Restore layer (this applies the Porter-Duff compositing)
1127+ canvas.restoreToCount(saveCount)
9151128 } else {
916- super .draw(canvas)
1129+ // No mask, draw normally
1130+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q &&
1131+ getUIManagerType(this ) == UIManagerType .FABRIC &&
1132+ needsIsolatedLayer(this )
1133+ ) {
1134+ val overflowInset = overflowInset
1135+ canvas.saveLayer(
1136+ overflowInset.left.toFloat(),
1137+ overflowInset.top.toFloat(),
1138+ (width + - overflowInset.right).toFloat(),
1139+ (height + - overflowInset.bottom).toFloat(),
1140+ null ,
1141+ )
1142+ super .draw(canvas)
1143+ canvas.restore()
1144+ } else {
1145+ super .draw(canvas)
1146+ }
9171147 }
9181148 }
9191149
9201150 override fun dispatchDraw (canvas : Canvas ) {
1151+ // Masking is handled in draw() method to include both background and children
1152+ // Just draw children normally here
9211153 if (_overflow != Overflow .VISIBLE || getTag(R .id.filter) != null ) {
9221154 clipToPaddingBox(this , canvas)
9231155 }
9241156 super .dispatchDraw(canvas)
1157+
1158+ val maskPaint = Paint (Paint .ANTI_ALIAS_FLAG ).apply {
1159+ xfermode = PorterDuffXfermode (PorterDuff .Mode .DST_IN )
1160+ isFilterBitmap = true
1161+ }
1162+
1163+ val bitmap = maskBitmap
1164+ if (bitmap != null && width > 0 && height > 0 ) {
1165+ val srcRect = Rect (0 , 0 , bitmap.width, bitmap.height)
1166+ val dstRect = RectF (0f , 0f , width.toFloat(), height.toFloat())
1167+ canvas.drawBitmap(bitmap, srcRect, dstRect, maskPaint)
1168+ }
9251169 }
9261170
9271171 override fun drawChild (canvas : Canvas , child : View , drawingTime : Long ): Boolean {
0 commit comments