diff --git a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js index e739919845ee..c186586d9a09 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -127,6 +127,11 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { */ experimental_mixBlendMode: true, + /** + * Isolation + */ + isolation: true, + /* * BoxShadow */ diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts index ac67b1901b1c..016c8b860964 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts @@ -332,6 +332,7 @@ export interface ViewStyle extends FlexStyle, ShadowStyleIOS, TransformsStyle { * Controls whether the View can be the target of touch events. */ pointerEvents?: 'box-none' | 'none' | 'box-only' | 'auto' | undefined; + isolation?: 'auto' | 'isolate' | undefined; cursor?: CursorValue | undefined; } diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js index c92c63016dc7..16e44fdfaa51 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js @@ -792,6 +792,7 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{ experimental_filter?: $ReadOnlyArray | string, experimental_mixBlendMode?: ____BlendMode_Internal, experimental_backgroundImage?: $ReadOnlyArray | string, + isolation?: 'auto' | 'isolate', }>; export type ____ViewStyle_Internal = $ReadOnly<{ diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index dc58d59d1bdf..74abd17caf0a 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -8330,6 +8330,7 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{ experimental_filter?: $ReadOnlyArray | string, experimental_mixBlendMode?: ____BlendMode_Internal, experimental_backgroundImage?: $ReadOnlyArray | string, + isolation?: \\"auto\\" | \\"isolate\\", }>; export type ____ViewStyle_Internal = $ReadOnly<{ ...____ViewStyle_InternalCore, diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 9f8e13c3b207..aaf704a5647b 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -4685,6 +4685,12 @@ public final class com/facebook/react/uimanager/ReactInvalidPropertyException : public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V } +public abstract interface class com/facebook/react/uimanager/ReactMixBlendMode { + public abstract fun getMixBlendMode ()Landroid/graphics/Paint; + public abstract fun isBlendModeParent ()Z + public abstract fun setMixBlendMode (Landroid/graphics/Paint;)V +} + public abstract interface class com/facebook/react/uimanager/ReactOverflowView { public abstract fun getOverflow ()Ljava/lang/String; } @@ -6640,11 +6646,13 @@ public class com/facebook/react/views/image/ReactImageManager$$PropsSetter : com public final class com/facebook/react/views/image/ReactImageManager$Companion { } -public final class com/facebook/react/views/image/ReactImageView : com/facebook/drawee/view/GenericDraweeView { +public final class com/facebook/react/views/image/ReactImageView : com/facebook/drawee/view/GenericDraweeView, com/facebook/react/uimanager/ReactMixBlendMode { public static final field Companion Lcom/facebook/react/views/image/ReactImageView$Companion; public static final field REMOTE_IMAGE_FADE_DURATION_MS I public fun (Landroid/content/Context;Lcom/facebook/drawee/controller/AbstractDraweeControllerBuilder;Lcom/facebook/react/views/image/GlobalImageLoadListener;Ljava/lang/Object;)V + public fun getMixBlendMode ()Landroid/graphics/Paint; public fun hasOverlappingRendering ()Z + public fun isBlendModeParent ()Z public final fun maybeUpdateView ()V public fun onDraw (Landroid/graphics/Canvas;)V public fun setBackgroundColor (I)V @@ -6657,6 +6665,7 @@ public final class com/facebook/react/views/image/ReactImageView : com/facebook/ public final fun setFadeDuration (I)V public final fun setHeaders (Lcom/facebook/react/bridge/ReadableMap;)V public final fun setLoadingIndicatorSource (Ljava/lang/String;)V + public fun setMixBlendMode (Landroid/graphics/Paint;)V public final fun setOverlayColor (I)V public final fun setProgressiveRenderingEnabled (Z)V public final fun setResizeMethod (Lcom/facebook/react/views/image/ImageResizeMethod;)V @@ -8177,7 +8186,7 @@ public class com/facebook/react/views/view/ReactViewBackgroundDrawable : com/fac public fun (Landroid/content/Context;)V } -public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGroup, com/facebook/react/touch/ReactHitSlopView, com/facebook/react/touch/ReactInterceptingViewGroup, com/facebook/react/uimanager/ReactClippingViewGroup, com/facebook/react/uimanager/ReactOverflowViewWithInset, com/facebook/react/uimanager/ReactPointerEventsView, com/facebook/react/uimanager/ReactZIndexedViewGroup { +public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGroup, com/facebook/react/touch/ReactHitSlopView, com/facebook/react/touch/ReactInterceptingViewGroup, com/facebook/react/uimanager/ReactClippingViewGroup, com/facebook/react/uimanager/ReactMixBlendMode, com/facebook/react/uimanager/ReactOverflowViewWithInset, com/facebook/react/uimanager/ReactPointerEventsView, com/facebook/react/uimanager/ReactZIndexedViewGroup { public fun (Landroid/content/Context;)V public fun addView (Landroid/view/View;ILandroid/view/ViewGroup$LayoutParams;)V protected fun addViewInLayout (Landroid/view/View;ILandroid/view/ViewGroup$LayoutParams;Z)Z @@ -8185,17 +8194,20 @@ public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGro public fun dispatchGenericMotionEvent (Landroid/view/MotionEvent;)Z public fun dispatchProvideStructure (Landroid/view/ViewStructure;)V protected fun dispatchSetPressed (Z)V + public fun draw (Landroid/graphics/Canvas;)V protected fun drawChild (Landroid/graphics/Canvas;Landroid/view/View;J)Z protected fun getChildDrawingOrder (II)I public fun getChildVisibleRect (Landroid/view/View;Landroid/graphics/Rect;Landroid/graphics/Point;)Z public fun getClippingRect (Landroid/graphics/Rect;)V public fun getHitSlopRect ()Landroid/graphics/Rect; + public fun getMixBlendMode ()Landroid/graphics/Paint; public fun getOverflow ()Ljava/lang/String; public fun getOverflowInset ()Landroid/graphics/Rect; public fun getPointerEvents ()Lcom/facebook/react/uimanager/PointerEvents; public fun getRemoveClippedSubviews ()Z public fun getZIndexMappedChildIndex (I)I public fun hasOverlappingRendering ()Z + public fun isBlendModeParent ()Z protected fun onAttachedToWindow ()V public fun onInterceptTouchEvent (Landroid/view/MotionEvent;)Z protected fun onLayout (ZIIII)V @@ -8218,6 +8230,7 @@ public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGro public fun setBorderStyle (Ljava/lang/String;)V public fun setBorderWidth (IF)V public fun setHitSlopRect (Landroid/graphics/Rect;)V + public fun setMixBlendMode (Landroid/graphics/Paint;)V public fun setNeedsOffscreenAlphaCompositing (Z)V public fun setOnInterceptTouchEventListener (Lcom/facebook/react/touch/OnInterceptTouchEventListener;)V public fun setOpacityIfPossible (F)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index 28262eb7ce15..086bb3866ab6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -541,7 +541,10 @@ public static void apply( } } - if (mixBlendMode != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (view instanceof ReactMixBlendMode + && !((ReactMixBlendMode) view).isBlendModeParent() + && mixBlendMode != null + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { p = p == null ? new Paint() : p; p.setBlendMode(mixBlendMode); } @@ -667,6 +670,15 @@ protected void onAfterUpdateTransaction(@NonNull T view) { Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? (BlendMode) view.getTag(R.id.mix_blend_mode) : null; + + if (mixBlendMode != null + && view instanceof ReactMixBlendMode + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Paint blendModePaint = new Paint(); + blendModePaint.setBlendMode(mixBlendMode); + ((ReactMixBlendMode) view).setMixBlendMode(blendModePaint); + } + Boolean useHWLayer = (Boolean) view.getTag(R.id.use_hardware_layer); LayerEffectsHelper.apply(view, filter, mixBlendMode, useHWLayer); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactMixBlendMode.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactMixBlendMode.kt new file mode 100644 index 000000000000..d7d504c4aa47 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactMixBlendMode.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.uimanager + +import android.graphics.Paint + +/** + * This interface should be implemented by all [View] subclasses that want to use the mixBlendMode + * prop to blend with its parent. + */ +public interface ReactMixBlendMode { + public var mixBlendMode: Paint? + + public fun isBlendModeParent(): Boolean +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt index 4af4776d5934..97d32a78a335 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt @@ -58,6 +58,7 @@ import com.facebook.react.uimanager.FloatUtil.floatsEqual import com.facebook.react.uimanager.LengthPercentage import com.facebook.react.uimanager.LengthPercentageType import com.facebook.react.uimanager.PixelUtil.toPixelFromDIP +import com.facebook.react.uimanager.ReactMixBlendMode import com.facebook.react.uimanager.Spacing import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.style.BorderRadiusProp @@ -89,7 +90,7 @@ public class ReactImageView( private val draweeControllerBuilder: AbstractDraweeControllerBuilder<*, *, *, *>, private val globalImageLoadListener: GlobalImageLoadListener?, private var callerContext: Any? -) : GenericDraweeView(context, buildHierarchy(context)) { +) : GenericDraweeView(context, buildHierarchy(context)), ReactMixBlendMode { private val sources: MutableList = mutableListOf() internal var imageSource: ImageSource? = null @@ -117,6 +118,12 @@ public class ReactImageView( private val reactBackgroundManager = ReactViewBackgroundManager(this) private var resizeMethod = ImageResizeMethod.AUTO + override var mixBlendMode: Paint? = null + + override fun isBlendModeParent(): Boolean { + return false + } + init { reactBackgroundManager.setOverflow("hidden") // Workaround Android bug where ImageView visibility is not propagated to the Drawable, so you diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java index 1bb6209492e8..e9673c1171c1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java @@ -14,16 +14,19 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; +import android.os.Build; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewStructure; import android.view.animation.Animation; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; @@ -48,6 +51,7 @@ import com.facebook.react.uimanager.ReactClippingProhibitedView; import com.facebook.react.uimanager.ReactClippingViewGroup; import com.facebook.react.uimanager.ReactClippingViewGroupHelper; +import com.facebook.react.uimanager.ReactMixBlendMode; import com.facebook.react.uimanager.ReactOverflowViewWithInset; import com.facebook.react.uimanager.ReactPointerEventsView; import com.facebook.react.uimanager.ReactZIndexedViewGroup; @@ -74,7 +78,8 @@ public class ReactViewGroup extends ViewGroup ReactPointerEventsView, ReactHitSlopView, ReactZIndexedViewGroup, - ReactOverflowViewWithInset { + ReactOverflowViewWithInset, + ReactMixBlendMode { private static final int ARRAY_CAPACITY_INCREMENT = 12; private static final int DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT; @@ -130,6 +135,7 @@ public void onLayoutChange( private @Nullable Rect mClippingRect; private @Nullable Rect mHitSlopRect; private Overflow mOverflow; + private @Nullable Paint mMixBlendMode; private PointerEvents mPointerEvents; private @Nullable ChildrenLayoutChangeListener mChildrenLayoutChangeListener; private @Nullable CSSBackgroundDrawable mCSSBackgroundDrawable; @@ -864,6 +870,21 @@ public void setHitSlopRect(@Nullable Rect rect) { mHitSlopRect = rect; } + @Override + public @Nullable Paint getMixBlendMode() { + return mMixBlendMode; + } + + @Override + public void setMixBlendMode(@Nullable Paint p) { + mMixBlendMode = p; + } + + @Override + public boolean isBlendModeParent() { + return true; + } + public void setOverflow(@Nullable String overflow) { mOverflow = overflow == null ? Overflow.VISIBLE : Overflow.fromString(overflow); invalidate(); @@ -885,6 +906,12 @@ public void setOverflow(@Nullable String overflow) { @Override public void setOverflowInset(int left, int top, int right, int bottom) { + if (mOverflowInset.left != left + || mOverflowInset.top != top + || mOverflowInset.right != right + || mOverflowInset.bottom != bottom) { + invalidate(); + } mOverflowInset.set(left, top, right, bottom); } @@ -904,6 +931,38 @@ public Rect getOverflowInset() { super.setBackground(drawable); } + @Override + public void draw(@NonNull Canvas canvas) { + if (ViewUtil.getUIManagerType(this) == UIManagerType.FABRIC) { + boolean needsOffscreenRender = false; + for (int i = 0; i < getChildCount(); i++) { + if (getChildAt(i) instanceof ReactMixBlendMode + && ((ReactMixBlendMode) getChildAt(i)).getMixBlendMode() != null) { + needsOffscreenRender = true; + } + } + + // Check if the view is a stacking context (has children) if it does, do the rendering + // offscreen and then composite back. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + && (needsOffscreenRender || mMixBlendMode != null)) { + Rect overflowInset = getOverflowInset(); + canvas.saveLayer( + overflowInset.left, + overflowInset.top, + getWidth() + -overflowInset.right, + getHeight() + -overflowInset.bottom, + mMixBlendMode); + super.draw(canvas); + canvas.restore(); + } else { + super.draw(canvas); + } + } else { + super.draw(canvas); + } + } + @Override protected void dispatchDraw(Canvas canvas) { if (ReactNativeFeatureFlags.enableBackgroundStyleApplicator()) { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp index f23240ec914b..8228b501f659 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp @@ -184,6 +184,14 @@ BaseViewProps::BaseViewProps( "experimental_mixBlendMode", sourceProps.mixBlendMode, {})), + isolation( + CoreFeatures::enablePropIteratorSetter ? sourceProps.isolation + : convertRawProp( + context, + rawProps, + "isolation", + sourceProps.isolation, + {})), transform( CoreFeatures::enablePropIteratorSetter ? sourceProps.transform : convertRawProp( @@ -330,6 +338,7 @@ void BaseViewProps::setProp( RAW_SET_PROP_SWITCH_CASE_BASIC(shouldRasterize); RAW_SET_PROP_SWITCH_CASE_BASIC(zIndex); RAW_SET_PROP_SWITCH_CASE_BASIC(pointerEvents); + RAW_SET_PROP_SWITCH_CASE_BASIC(isolation); RAW_SET_PROP_SWITCH_CASE_BASIC(hitSlop); RAW_SET_PROP_SWITCH_CASE_BASIC(onLayout); RAW_SET_PROP_SWITCH_CASE_BASIC(collapsable); diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h index 1e37eec1d697..edc01556c914 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -68,7 +69,10 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps { std::vector backgroundImage{}; // MixBlendMode - BlendMode mixBlendMode; + BlendMode mixBlendMode{BlendMode::Normal}; + + // Isolate + Isolation isolation{Isolation::Auto}; // Transform Transform transform{}; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp index 49099f87ef7e..ef13a094524f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp @@ -63,6 +63,7 @@ void ViewShadowNode::initialize() noexcept { viewProps.removeClippedSubviews || viewProps.cursor != Cursor::Auto || !viewProps.filter.empty() || viewProps.mixBlendMode != BlendMode::Normal || + viewProps.isolation == Isolation::Isolate || HostPlatformViewTraitsInitializer::formsStackingContext(viewProps); bool formsView = formsStackingContext || diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h index fdb4251b16e3..6681c69dfeb9 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -1184,6 +1185,27 @@ inline void fromRawValue( result = backgroundImage; } +inline void fromRawValue( + const PropsParserContext& /*context*/, + const RawValue& value, + Isolation& result) { + react_native_expect(value.hasType()); + result = Isolation::Auto; + if (!value.hasType()) { + return; + } + + auto rawIsolation = static_cast(value); + std::optional isolation = isolationFromString(rawIsolation); + + if (!isolation) { + LOG(ERROR) << "Could not parse isolation: " << rawIsolation; + return; + } + + result = isolation.value(); +} + template inline std::string toString(const std::array vec) { std::string s; diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/BlendMode.h b/packages/react-native/ReactCommon/react/renderer/graphics/BlendMode.h index 9399ff8e4b52..1afd7864f1ff 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/BlendMode.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/BlendMode.h @@ -7,12 +7,8 @@ #pragma once -#include - #include -#include #include -#include namespace facebook::react { diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/Isolation.h b/packages/react-native/ReactCommon/react/renderer/graphics/Isolation.h new file mode 100644 index 000000000000..941c4a1c28ab --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/graphics/Isolation.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +// https://www.w3.org/TR/compositing-1/#isolation +enum class Isolation { + Auto, + Isolate, +}; + +inline std::optional isolationFromString( + std::string_view isolationSetting) { + if (isolationSetting == "auto") { + return Isolation::Auto; + } else if (isolationSetting == "isolate") { + return Isolation::Isolate; + } else { + return std::nullopt; + } +} +} // namespace facebook::react