Skip to content

Commit b1e9c2d

Browse files
committed
feat(android): masking using Drawable
1 parent 3fd956c commit b1e9c2d

File tree

6 files changed

+418
-278
lines changed

6 files changed

+418
-278
lines changed

packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@ const validAttributesForNonEventProps = {
203203
experimental_backgroundRepeat: {
204204
process: require('../StyleSheet/processBackgroundRepeat').default,
205205
},
206+
maskImage: ReactNativeFeatureFlags.enableNativeCSSParsing()
207+
? (true as const)
208+
: {process: require('../StyleSheet/processBackgroundImage').default},
206209
boxShadow: ReactNativeFeatureFlags.enableNativeCSSParsing()
207210
? (true as const)
208211
: {process: require('../StyleSheet/processBoxShadow').default},
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.uimanager.drawable
9+
10+
import android.graphics.Canvas
11+
import android.graphics.ColorFilter
12+
import android.graphics.Paint
13+
import android.graphics.PixelFormat
14+
import android.graphics.PorterDuff
15+
import android.graphics.PorterDuffXfermode
16+
import android.graphics.drawable.Animatable
17+
import android.graphics.drawable.Drawable
18+
import com.facebook.drawee.controller.ControllerListener
19+
import com.facebook.drawee.drawable.DrawableParent
20+
import com.facebook.drawee.generic.GenericDraweeHierarchy
21+
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder
22+
import com.facebook.drawee.view.DraweeHolder
23+
import com.facebook.imagepipeline.image.ImageInfo
24+
import com.facebook.react.views.image.ImageResizeMode
25+
26+
/**
27+
* A Drawable that uses DraweeHolder to load and display an image mask.
28+
* This avoids creating Bitmap copies and leverages Fresco's caching and lifecycle management.
29+
*
30+
* The mask is applied using Porter-Duff DST_IN mode - the Drawable's alpha channel
31+
* determines what parts of the view are visible.
32+
*/
33+
internal class DraweeMaskDrawable(
34+
resources: android.content.res.Resources,
35+
) : Drawable(), MaskDrawable {
36+
37+
private val draweeHolder: DraweeHolder<GenericDraweeHierarchy> =
38+
DraweeHolder(GenericDraweeHierarchyBuilder.newInstance(resources).setFadeDuration(0).build())
39+
40+
41+
// Note: Porter-Duff compositing is applied at the view level, not here
42+
// This Drawable just draws the image normally - its alpha channel will be used for masking
43+
44+
init {
45+
// Set ourselves as callback to invalidate when DraweeDrawable changes
46+
// This will be updated when the controller is set
47+
}
48+
49+
/**
50+
* Sets the ImageRequest to load as the mask.
51+
* The DraweeHolder will handle loading, caching, and lifecycle management.
52+
*/
53+
fun setImageRequest(imageRequest: com.facebook.imagepipeline.request.ImageRequest?) {
54+
if (imageRequest == null) {
55+
draweeHolder.controller = null
56+
return
57+
}
58+
59+
val controllerBuilder = com.facebook.drawee.backends.pipeline.Fresco.newDraweeControllerBuilder()
60+
controllerBuilder
61+
.setImageRequest(imageRequest)
62+
.setAutoPlayAnimations(true)
63+
.setOldController(draweeHolder.controller)
64+
65+
draweeHolder.controller = controllerBuilder.build()
66+
controllerBuilder.reset()
67+
}
68+
69+
/**
70+
* Attaches the DraweeHolder (should be called when view is attached to window)
71+
*/
72+
override fun onAttach() {
73+
draweeHolder.onAttach()
74+
}
75+
76+
/**
77+
* Detaches the DraweeHolder (should be called when view is detached from window)
78+
*/
79+
fun onDetach() {
80+
draweeHolder.onDetach()
81+
}
82+
83+
override fun draw(canvas: Canvas) {
84+
val drawable = draweeHolder.topLevelDrawable ?: return
85+
val bounds = bounds
86+
87+
// Set bounds if not already set
88+
if (drawable.bounds.isEmpty) {
89+
drawable.bounds = bounds
90+
}
91+
92+
// Draw the DraweeDrawable normally
93+
// The Porter-Duff DST_IN mode will be applied at the view level when this Drawable
94+
// is drawn with a Paint that has the xfermode set
95+
drawable.draw(canvas)
96+
}
97+
98+
/**
99+
* Draws this Drawable with Porter-Duff compositing applied.
100+
* This is called from ReactViewGroup.draw() to apply the mask.
101+
*
102+
* Since we're already inside a saveLayer in ReactViewGroup.draw(), we create
103+
* a nested saveLayer with Porter-Duff Paint. When we restore this inner layer,
104+
* it composites with the outer layer using Porter-Duff DST_IN mode.
105+
*
106+
* This avoids creating Bitmap copies - the DraweeDrawable is drawn directly.
107+
*/
108+
override fun drawWithMaskMode(canvas: Canvas, maskPaint: Paint) {
109+
val drawable = draweeHolder.topLevelDrawable ?: return
110+
val bounds = bounds
111+
112+
if (bounds.isEmpty) {
113+
return
114+
}
115+
116+
// Set bounds if not already set
117+
if (drawable.bounds.isEmpty) {
118+
drawable.bounds = bounds
119+
}
120+
121+
// Create a nested saveLayer with Porter-Duff DST_IN mode
122+
// When we restore this layer, it will composite with the outer layer (which contains
123+
// the background + children) using DST_IN mode, effectively masking the content
124+
val saveCount = canvas.saveLayer(
125+
bounds.left.toFloat(),
126+
bounds.top.toFloat(),
127+
bounds.right.toFloat(),
128+
bounds.bottom.toFloat(),
129+
maskPaint
130+
)
131+
132+
// Draw the DraweeDrawable to the inner layer
133+
drawable.draw(canvas)
134+
135+
// Restore the inner layer - this composites it with the outer layer using DST_IN mode
136+
canvas.restoreToCount(saveCount)
137+
}
138+
139+
override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
140+
super.setBounds(left, top, right, bottom)
141+
draweeHolder.topLevelDrawable?.setBounds(left, top, right, bottom)
142+
}
143+
144+
override fun setAlpha(alpha: Int) {
145+
draweeHolder.topLevelDrawable?.alpha = alpha
146+
invalidateSelf()
147+
}
148+
149+
override fun setColorFilter(colorFilter: ColorFilter?) {
150+
draweeHolder.topLevelDrawable?.colorFilter = colorFilter
151+
invalidateSelf()
152+
}
153+
154+
@Deprecated("Deprecated in Java")
155+
override fun getOpacity(): Int {
156+
return draweeHolder.topLevelDrawable?.opacity ?: PixelFormat.TRANSLUCENT
157+
}
158+
}
159+
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.uimanager.drawable
9+
10+
import android.graphics.Canvas
11+
import android.graphics.ColorFilter
12+
import android.graphics.Paint
13+
import android.graphics.PixelFormat
14+
import android.graphics.PorterDuff
15+
import android.graphics.PorterDuffXfermode
16+
import android.graphics.Shader
17+
import android.graphics.drawable.Drawable
18+
19+
/**
20+
* A Drawable that draws a gradient shader for masking.
21+
* This avoids creating Bitmap copies - the gradient is drawn directly using a Shader.
22+
*
23+
* The mask is applied using Porter-Duff DST_IN mode - the Drawable's alpha channel
24+
* determines what parts of the view are visible.
25+
*/
26+
internal class GradientMaskDrawable(
27+
private var shader: Shader? = null,
28+
) : Drawable(), MaskDrawable {
29+
30+
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
31+
shader = this@GradientMaskDrawable.shader
32+
}
33+
34+
/**
35+
* Sets the Shader to use for the gradient mask.
36+
* The Shader will be drawn directly - no Bitmap conversion needed.
37+
*/
38+
fun setShader(shader: Shader?) {
39+
if (this.shader != shader) {
40+
this.shader = shader
41+
paint.shader = shader
42+
invalidateSelf()
43+
}
44+
}
45+
46+
override fun draw(canvas: Canvas) {
47+
val shader = this.shader ?: return
48+
val bounds = bounds
49+
50+
if (bounds.isEmpty) {
51+
return
52+
}
53+
54+
// Draw the gradient shader directly
55+
canvas.drawRect(
56+
bounds.left.toFloat(),
57+
bounds.top.toFloat(),
58+
bounds.right.toFloat(),
59+
bounds.bottom.toFloat(),
60+
paint
61+
)
62+
}
63+
64+
/**
65+
* Draws this Drawable with Porter-Duff compositing applied.
66+
* This is called from ReactViewGroup.draw() to apply the mask.
67+
*
68+
* Since we're already inside a saveLayer in ReactViewGroup.draw(), we create
69+
* a nested saveLayer with Porter-Duff Paint. When we restore this inner layer,
70+
* it composites with the outer layer using Porter-Duff DST_IN mode.
71+
*/
72+
override fun drawWithMaskMode(canvas: Canvas, maskPaint: Paint) {
73+
val shader = this.shader ?: return
74+
val bounds = bounds
75+
76+
if (bounds.isEmpty) {
77+
return
78+
}
79+
80+
// Create a nested saveLayer with Porter-Duff DST_IN mode
81+
// When we restore this layer, it will composite with the outer layer (which contains
82+
// the background + children) using DST_IN mode, effectively masking the content
83+
val saveCount = canvas.saveLayer(
84+
bounds.left.toFloat(),
85+
bounds.top.toFloat(),
86+
bounds.right.toFloat(),
87+
bounds.bottom.toFloat(),
88+
maskPaint
89+
)
90+
91+
// Draw the gradient shader to the inner layer
92+
canvas.drawRect(
93+
bounds.left.toFloat(),
94+
bounds.top.toFloat(),
95+
bounds.right.toFloat(),
96+
bounds.bottom.toFloat(),
97+
paint
98+
)
99+
100+
// Restore the inner layer - this composites it with the outer layer using DST_IN mode
101+
canvas.restoreToCount(saveCount)
102+
}
103+
104+
override fun onAttach() {
105+
}
106+
107+
override fun setAlpha(alpha: Int) {
108+
paint.alpha = alpha
109+
invalidateSelf()
110+
}
111+
112+
override fun setColorFilter(colorFilter: ColorFilter?) {
113+
paint.colorFilter = colorFilter
114+
invalidateSelf()
115+
}
116+
117+
@Deprecated("Deprecated in Java")
118+
override fun getOpacity(): Int {
119+
return PixelFormat.TRANSLUCENT
120+
}
121+
}
122+
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.uimanager.drawable
9+
10+
import android.graphics.Canvas
11+
import android.graphics.Paint
12+
import android.graphics.drawable.Drawable
13+
14+
/**
15+
* Common interface for mask Drawables that can be used to mask view content.
16+
* Both DraweeMaskDrawable and GradientMaskDrawable implement this interface.
17+
*/
18+
internal interface MaskDrawable {
19+
/**
20+
* Draws this Drawable with Porter-Duff compositing applied.
21+
* This is called from ReactViewGroup.draw() to apply the mask.
22+
*/
23+
fun drawWithMaskMode(canvas: Canvas, maskPaint: Paint)
24+
fun onAttach()
25+
}

0 commit comments

Comments
 (0)