Skip to content

Commit ee970f9

Browse files
committed
feat(android): wip
1 parent b1e9c2d commit ee970f9

File tree

11 files changed

+425
-167
lines changed

11 files changed

+425
-167
lines changed

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import android.graphics.Insets;
2020
import android.graphics.Paint;
2121
import android.graphics.Point;
22+
import android.graphics.PorterDuff;
23+
import android.graphics.PorterDuffXfermode;
2224
import android.graphics.Rect;
2325
import android.os.Build;
2426
import android.os.Bundle;
@@ -70,7 +72,9 @@
7072
import com.facebook.react.uimanager.UIManagerHelper;
7173
import com.facebook.react.uimanager.common.UIManagerType;
7274
import com.facebook.react.uimanager.common.ViewUtil;
75+
import com.facebook.react.uimanager.drawable.GradientMaskDrawable;
7376
import com.facebook.react.uimanager.events.EventDispatcher;
77+
import com.facebook.react.uimanager.style.BackgroundImageLayer;
7478
import com.facebook.systrace.Systrace;
7579
import java.util.concurrent.atomic.AtomicInteger;
7680

@@ -313,19 +317,40 @@ protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
313317
&& ViewUtil.getUIManagerType(this) == UIManagerType.FABRIC
314318
&& needsIsolatedLayer(this)) {
315319
mixBlendMode = (BlendMode) child.getTag(R.id.mix_blend_mode);
320+
316321
if (mixBlendMode != null) {
317322
Paint p = new Paint();
318323
p.setBlendMode(mixBlendMode);
319324
canvas.saveLayer(0, 0, getWidth(), getHeight(), p);
320325
}
321326
}
322327

328+
// BackgroundImageLayer mask = (BackgroundImageLayer)child.getTag(R.id.mask_gradient_layer);
329+
// int saveCount = -1;
330+
// if (mask != null) {
331+
// saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null);
332+
// }
333+
323334
boolean result = super.drawChild(canvas, child, drawingTime);
324335

325336
if (mixBlendMode != null) {
326337
canvas.restore();
327338
}
328339

340+
// if (mask != null) {
341+
// Paint maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
342+
// maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
343+
// maskPaint.setFilterBitmap(true);
344+
// GradientMaskDrawable maskDrawable = new GradientMaskDrawable();
345+
// maskDrawable.setShader(mask.getShader(child.getWidth(), child.getHeight()));
346+
// maskDrawable.setBounds(0, 0, child.getWidth(), child.getHeight());
347+
// maskDrawable.drawWithMaskMode(canvas, maskPaint);
348+
// }
349+
//
350+
// if (saveCount != -1) {
351+
// canvas.restoreToCount(saveCount);
352+
// }
353+
329354
return result;
330355
}
331356

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

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@ import android.os.Build
2020
import android.view.View
2121
import android.widget.ImageView
2222
import androidx.annotation.ColorInt
23+
import com.facebook.imagepipeline.request.ImageRequest
24+
import com.facebook.imagepipeline.request.ImageRequestBuilder
25+
import com.facebook.react.R
2326
import com.facebook.react.bridge.ReadableArray
27+
import com.facebook.react.bridge.ReadableMap
2428
import com.facebook.react.common.annotations.UnstableReactNativeAPI
2529
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
30+
import com.facebook.react.modules.fresco.ReactNetworkImageRequest
2631
import com.facebook.react.uimanager.PixelUtil.dpToPx
2732
import com.facebook.react.uimanager.PixelUtil.pxToDp
2833
import com.facebook.react.uimanager.common.UIManagerType
@@ -31,9 +36,12 @@ import com.facebook.react.uimanager.drawable.BackgroundDrawable
3136
import com.facebook.react.uimanager.drawable.BackgroundImageDrawable
3237
import com.facebook.react.uimanager.drawable.BorderDrawable
3338
import com.facebook.react.uimanager.drawable.CompositeBackgroundDrawable
39+
import com.facebook.react.uimanager.drawable.DraweeMaskDrawable
40+
import com.facebook.react.uimanager.drawable.GradientMaskDrawable
3441
import com.facebook.react.uimanager.drawable.InsetBoxShadowDrawable
3542
import com.facebook.react.uimanager.drawable.MIN_INSET_BOX_SHADOW_SDK_VERSION
3643
import com.facebook.react.uimanager.drawable.MIN_OUTSET_BOX_SHADOW_SDK_VERSION
44+
import com.facebook.react.uimanager.drawable.MaskDrawable
3745
import com.facebook.react.uimanager.drawable.OutlineDrawable
3846
import com.facebook.react.uimanager.drawable.OutsetBoxShadowDrawable
3947
import com.facebook.react.uimanager.style.BackgroundImageLayer
@@ -47,6 +55,8 @@ import com.facebook.react.uimanager.style.BorderStyle
4755
import com.facebook.react.uimanager.style.BoxShadow
4856
import com.facebook.react.uimanager.style.LogicalEdge
4957
import com.facebook.react.uimanager.style.OutlineStyle
58+
import com.facebook.react.views.imagehelper.ImageSource
59+
import com.facebook.react.views.view.ReactViewGroup
5060

5161
/**
5262
* Utility object responsible for applying backgrounds, borders, and related visual effects to
@@ -795,4 +805,121 @@ public object BackgroundStyleApplicator {
795805
)
796806
return paddingBoxPath
797807
}
808+
809+
@JvmStatic
810+
public fun setMaskImage(
811+
view: View,
812+
maskImage: ReadableArray?,
813+
) {
814+
val composite = ensureCompositeBackgroundDrawable(view)
815+
if (ViewUtil.getUIManagerType(view) == UIManagerType.FABRIC) {
816+
// Set SOFTWARE layer type immediately when mask is requested
817+
// This ensures Porter-Duff compositing works even if view is drawn before image loads
818+
if (maskImage != null && maskImage.size() > 0) {
819+
view.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
820+
} else {
821+
view.setLayerType(View.LAYER_TYPE_NONE, null)
822+
}
823+
824+
(composite.mask as? DraweeMaskDrawable)?.onDetach()
825+
826+
if (maskImage != null && maskImage.size() > 0) {
827+
// Use the first mask layer
828+
val maskImageMap = maskImage.getMap(0)
829+
val type = maskImageMap?.getString("type")
830+
831+
when (type) {
832+
"image" -> {
833+
// Load image mask using DraweeHolder (no Bitmap copy needed)
834+
val url = maskImageMap.getString("url")
835+
if (url != null) {
836+
loadMaskDrawable(view, url)
837+
}
838+
}
839+
"linear-gradient", "radial-gradient" -> {
840+
// Create gradient mask from shader (still uses Bitmap for gradients)
841+
createGradientMask(view, maskImageMap)
842+
}
843+
else -> {
844+
composite.mask = null
845+
}
846+
}
847+
} else {
848+
composite.mask = null
849+
}
850+
if (composite.mask != null && composite.mask is DraweeMaskDrawable) {
851+
(composite.mask as DraweeMaskDrawable).onAttach()
852+
}
853+
}
854+
}
855+
856+
857+
/**
858+
* Creates a gradient mask from a gradient definition. Uses a GradientMaskDrawable
859+
* to draw the gradient directly without creating a Bitmap copy.
860+
*/
861+
private fun createGradientMask(view: View, gradientMap: ReadableMap) {
862+
val gradientLayer = BackgroundImageLayer.parse(gradientMap, view.context)
863+
val composite = ensureCompositeBackgroundDrawable(view)
864+
if (gradientLayer != null) {
865+
// Create or get existing GradientMaskDrawable
866+
val gradientDrawable = composite.mask as? GradientMaskDrawable
867+
?: GradientMaskDrawable().also {
868+
composite.mask = it
869+
}
870+
871+
// Generate mask when view has dimensions
872+
view.post {
873+
if (view.width > 0 && view.height > 0) {
874+
val shader = gradientLayer.getShader(view.width.toFloat(), view.height.toFloat())
875+
gradientDrawable.setShader(shader)
876+
gradientDrawable.setBounds(0, 0, view.width, view.height)
877+
composite.mask = gradientDrawable
878+
println("Gradient mask created: ${view.width}x${view.height}")
879+
view.setTag(R.id.mask_gradient_layer, gradientLayer)
880+
view.invalidate()
881+
} else {
882+
// Store gradient for later when view gets dimensions
883+
view.setTag(R.id.mask_gradient_layer, gradientLayer)
884+
}
885+
}
886+
} else {
887+
composite.mask = null
888+
}
889+
}
890+
891+
/**
892+
* Loads an image mask using DraweeHolder. This avoids creating Bitmap copies and leverages
893+
* Fresco's caching and lifecycle management. The DraweeDrawable will be used directly for masking.
894+
*/
895+
private fun loadMaskDrawable(view: View, url: String) {
896+
val imageSource = ImageSource(view.context, url)
897+
val composite = ensureCompositeBackgroundDrawable(view)
898+
val imageRequest: ImageRequest =
899+
ReactNetworkImageRequest.fromBuilderWithHeaders(
900+
ImageRequestBuilder.newBuilderWithSource(imageSource.uri),
901+
null,
902+
imageSource.cacheControl
903+
)
904+
905+
// Create or get existing DraweeMaskDrawable
906+
val maskDrawable = (composite.mask as DraweeMaskDrawable?)
907+
?: DraweeMaskDrawable(view.context.resources).also {
908+
composite.mask = it
909+
}
910+
911+
// Set the ImageRequest - DraweeHolder will handle loading, caching, and lifecycle
912+
maskDrawable.setImageRequest(imageRequest)
913+
914+
// Update bounds if view already has dimensions
915+
if (view.width > 0 && view.height > 0) {
916+
maskDrawable.setBounds(0, 0, view.width, view.height)
917+
}
918+
919+
composite.mask = maskDrawable
920+
view.invalidate()
921+
}
922+
923+
@JvmStatic
924+
internal fun getMask(view: View): MaskDrawable? = ensureCompositeBackgroundDrawable(view).mask
798925
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,11 @@ public void setBoxShadow(T view, @Nullable ReadableArray shadows) {
860860
BackgroundStyleApplicator.setBoxShadow(view, shadows);
861861
}
862862

863+
@ReactProp(name = ViewProps.MASK_IMAGE, customType = "BackgroundImage")
864+
public void setMaskImage(T view, @Nullable ReadableArray images) {
865+
BackgroundStyleApplicator.setMaskImage(view, images);
866+
}
867+
863868
private void logUnsupportedPropertyWarning(String propName) {
864869
FLog.w(ReactConstants.TAG, "%s doesn't support property '%s'", getName(), propName);
865870
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ public abstract class BaseViewManagerDelegate<
8888

8989
ViewProps.MIX_BLEND_MODE -> mViewManager.setMixBlendMode(view, value as String?)
9090

91+
ViewProps.MASK_IMAGE -> mViewManager.setMaskImage(view, value as ReadableArray?)
92+
9193
ViewProps.SHADOW_COLOR ->
9294
mViewManager.setShadowColor(view, ColorPropConverter.getColor(value, view.context, 0))
9395

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,21 @@
88
package com.facebook.react.uimanager.drawable
99

1010
import android.content.Context
11+
import android.graphics.Canvas
1112
import android.graphics.Outline
13+
import android.graphics.Paint
1214
import android.graphics.Path
15+
import android.graphics.PorterDuff
16+
import android.graphics.PorterDuffXfermode
1317
import android.graphics.RectF
1418
import android.graphics.drawable.Drawable
1519
import android.graphics.drawable.LayerDrawable
1620
import android.os.Build
1721
import com.facebook.react.common.annotations.UnstableReactNativeAPI
22+
import com.facebook.react.uimanager.BlendModeHelper.needsIsolatedLayer
1823
import com.facebook.react.uimanager.PixelUtil.dpToPx
24+
import com.facebook.react.uimanager.common.UIManagerType
25+
import com.facebook.react.uimanager.common.ViewUtil.getUIManagerType
1926
import com.facebook.react.uimanager.style.BorderInsets
2027
import com.facebook.react.uimanager.style.BorderRadiusStyle
2128

@@ -58,6 +65,10 @@ internal class CompositeBackgroundDrawable(
5865

5966
// Holder value for currently set border radius
6067
var borderRadius: BorderRadiusStyle? = null,
68+
69+
// Mask
70+
var mask: MaskDrawable? = null
71+
6172
) :
6273
LayerDrawable(
6374
createLayersArray(
@@ -223,6 +234,43 @@ internal class CompositeBackgroundDrawable(
223234
}
224235
}
225236

237+
// override fun draw(canvas: Canvas) {
238+
// val drawable = mask
239+
// val width = bounds.width()
240+
// val height = bounds.height()
241+
// var saveCount: Int? = null;
242+
// val useMask = drawable != null && width > 0 && height > 0
243+
// if (useMask) {
244+
// val bounds = RectF(0f, 0f, width.toFloat(), height.toFloat())
245+
// saveCount = canvas.saveLayer(bounds, null)
246+
// }
247+
//
248+
// super.draw(canvas)
249+
// if (useMask) {
250+
//
251+
// // Now apply the mask Drawable to everything that was drawn (background + children)
252+
// // Use Porter-Duff DST_IN mode - the Drawable's alpha channel determines visibility
253+
// val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
254+
// xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
255+
// isFilterBitmap = true
256+
// }
257+
//
258+
// if (drawable is DraweeMaskDrawable) {
259+
// (drawable as DraweeMaskDrawable?)?.setBounds(0, 0, width, height)
260+
// }
261+
//
262+
// // Draw the mask Drawable with Porter-Duff DST_IN mode
263+
// // This will mask everything drawn before (background + children)
264+
//// drawable.drawWithMaskMode(canvas, maskPaint)
265+
//
266+
// // Restore layer (this applies the Porter-Duff compositing)
267+
// if (saveCount != null) {
268+
// canvas.restoreToCount(saveCount)
269+
// }
270+
// }
271+
// }
272+
273+
226274
companion object {
227275
private fun createLayersArray(
228276
originalBackground: Drawable?,

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ internal class DraweeMaskDrawable(
4545
// Set ourselves as callback to invalidate when DraweeDrawable changes
4646
// This will be updated when the controller is set
4747
}
48-
49-
/**
48+
49+
/**
5050
* Sets the ImageRequest to load as the mask.
5151
* The DraweeHolder will handle loading, caching, and lifecycle management.
5252
*/
@@ -155,5 +155,14 @@ internal class DraweeMaskDrawable(
155155
override fun getOpacity(): Int {
156156
return draweeHolder.topLevelDrawable?.opacity ?: PixelFormat.TRANSLUCENT
157157
}
158+
159+
160+
override fun getIntrinsicWidth(): Int {
161+
return draweeHolder.topLevelDrawable?.intrinsicWidth ?: -1
162+
}
163+
164+
override fun getIntrinsicHeight(): Int {
165+
return draweeHolder.topLevelDrawable?.intrinsicHeight ?: -1
166+
}
158167
}
159168

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ import android.graphics.drawable.Drawable
1515
* Common interface for mask Drawables that can be used to mask view content.
1616
* Both DraweeMaskDrawable and GradientMaskDrawable implement this interface.
1717
*/
18-
internal interface MaskDrawable {
18+
public interface MaskDrawable {
1919
/**
2020
* Draws this Drawable with Porter-Duff compositing applied.
2121
* This is called from ReactViewGroup.draw() to apply the mask.
2222
*/
23-
fun drawWithMaskMode(canvas: Canvas, maskPaint: Paint)
24-
fun onAttach()
23+
public fun drawWithMaskMode(canvas: Canvas, maskPaint: Paint)
24+
public fun onAttach()
2525
}

0 commit comments

Comments
 (0)