Skip to content

Commit 56a40bf

Browse files
committed
feat(android): initial masking functionality
1 parent cf69230 commit 56a40bf

File tree

3 files changed

+431
-21
lines changed

3 files changed

+431
-21
lines changed

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt

Lines changed: 264 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,17 @@ package com.facebook.react.views.view
1212
import android.annotation.SuppressLint
1313
import android.annotation.TargetApi
1414
import android.content.Context
15+
import android.graphics.Bitmap
1516
import android.graphics.BlendMode
1617
import android.graphics.Canvas
1718
import android.graphics.Paint
19+
import android.graphics.Path
20+
import android.graphics.PorterDuff
21+
import android.graphics.PorterDuffXfermode
22+
import android.graphics.RadialGradient
1823
import android.graphics.Rect
24+
import android.graphics.RectF
25+
import android.graphics.Shader
1926
import android.graphics.drawable.Drawable
2027
import android.os.Build
2128
import android.view.MotionEvent
@@ -67,6 +74,7 @@ import com.facebook.react.uimanager.style.LogicalEdge
6774
import com.facebook.react.uimanager.style.Overflow
6875
import com.facebook.react.views.view.CanvasUtil.enableZ
6976
import java.util.ArrayList
77+
import java.util.Random
7078
import kotlin.concurrent.Volatile
7179
import 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

Comments
 (0)