diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js index 0f00224adfff..2db5431fc564 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js @@ -778,8 +778,8 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{ elevation?: number, pointerEvents?: 'auto' | 'none' | 'box-none' | 'box-only', cursor?: CursorValue, - experimental_boxShadow?: $ReadOnlyArray, - experimental_filter?: $ReadOnlyArray, + experimental_boxShadow?: $ReadOnlyArray | string, + experimental_filter?: $ReadOnlyArray | string, experimental_mixBlendMode?: ____BlendMode_Internal, }>; 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 910d0b3f0f4f..b5532794cb6d 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 @@ -8776,8 +8776,8 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{ elevation?: number, pointerEvents?: \\"auto\\" | \\"none\\" | \\"box-none\\" | \\"box-only\\", cursor?: CursorValue, - experimental_boxShadow?: $ReadOnlyArray, - experimental_filter?: $ReadOnlyArray, + experimental_boxShadow?: $ReadOnlyArray | string, + experimental_filter?: $ReadOnlyArray | string, experimental_mixBlendMode?: ____BlendMode_Internal, }>; export type ____ViewStyle_Internal = $ReadOnly<{ diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 8afeee79e05f..13455f62aa46 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3969,6 +3969,22 @@ public abstract interface class com/facebook/react/turbomodule/core/interfaces/T public abstract fun getBindingsInstaller ()Lcom/facebook/react/turbomodule/core/interfaces/BindingsInstallerHolder; } +public final class com/facebook/react/uimanager/BackgroundStyleApplicator { + public static final field INSTANCE Lcom/facebook/react/uimanager/BackgroundStyleApplicator; + public static final fun clipToPaddingBox (Landroid/view/View;Landroid/graphics/Canvas;)V + public static final fun getBackgroundColor (Landroid/view/View;)Ljava/lang/Integer; + public static final fun getBorderColor (Landroid/view/View;Lcom/facebook/react/uimanager/style/LogicalEdge;)Ljava/lang/Integer; + public static final fun getBorderRadius (Landroid/view/View;Lcom/facebook/react/uimanager/style/BorderRadiusProp;)Lcom/facebook/react/uimanager/LengthPercentage; + public static final fun getBorderStyle (Landroid/view/View;)Lcom/facebook/react/uimanager/style/BorderStyle; + public static final fun getBorderWidth (Landroid/view/View;Lcom/facebook/react/uimanager/style/LogicalEdge;)Ljava/lang/Float; + public static final fun setBackgroundColor (Landroid/view/View;Ljava/lang/Integer;)V + public static final fun setBorderColor (Landroid/view/View;Lcom/facebook/react/uimanager/style/LogicalEdge;Ljava/lang/Integer;)V + public static final fun setBorderRadius (Landroid/view/View;Lcom/facebook/react/uimanager/style/BorderRadiusProp;Lcom/facebook/react/uimanager/LengthPercentage;)V + public static final fun setBorderStyle (Landroid/view/View;Lcom/facebook/react/uimanager/style/BorderStyle;)V + public static final fun setBorderWidth (Landroid/view/View;Lcom/facebook/react/uimanager/style/LogicalEdge;Ljava/lang/Float;)V + public static final fun setShadows (Landroid/view/View;Ljava/util/List;)V +} + public abstract class com/facebook/react/uimanager/BaseViewManager : com/facebook/react/uimanager/ViewManager, android/view/View$OnLayoutChangeListener, com/facebook/react/uimanager/BaseViewManagerInterface { public fun ()V public fun (Lcom/facebook/react/bridge/ReactApplicationContext;)V @@ -5915,6 +5931,49 @@ public final class com/facebook/react/uimanager/style/BorderRadiusStyle { public fun toString ()Ljava/lang/String; } +public final class com/facebook/react/uimanager/style/BorderStyle : java/lang/Enum { + public static final field Companion Lcom/facebook/react/uimanager/style/BorderStyle$Companion; + public static final field DASHED Lcom/facebook/react/uimanager/style/BorderStyle; + public static final field DOTTED Lcom/facebook/react/uimanager/style/BorderStyle; + public static final field SOLID Lcom/facebook/react/uimanager/style/BorderStyle; + public static final fun fromString (Ljava/lang/String;)Lcom/facebook/react/uimanager/style/BorderStyle; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/facebook/react/uimanager/style/BorderStyle; + public static fun values ()[Lcom/facebook/react/uimanager/style/BorderStyle; +} + +public final class com/facebook/react/uimanager/style/BorderStyle$Companion { + public final fun fromString (Ljava/lang/String;)Lcom/facebook/react/uimanager/style/BorderStyle; +} + +public final class com/facebook/react/uimanager/style/BoxShadow { + public static final field Companion Lcom/facebook/react/uimanager/style/BoxShadow$Companion; + public fun (FFLjava/lang/Integer;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Boolean;)V + public synthetic fun (FFLjava/lang/Integer;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()F + public final fun component2 ()F + public final fun component3 ()Ljava/lang/Integer; + public final fun component4 ()Ljava/lang/Float; + public final fun component5 ()Ljava/lang/Float; + public final fun component6 ()Ljava/lang/Boolean; + public final fun copy (FFLjava/lang/Integer;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Boolean;)Lcom/facebook/react/uimanager/style/BoxShadow; + public static synthetic fun copy$default (Lcom/facebook/react/uimanager/style/BoxShadow;FFLjava/lang/Integer;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Boolean;ILjava/lang/Object;)Lcom/facebook/react/uimanager/style/BoxShadow; + public fun equals (Ljava/lang/Object;)Z + public final fun getBlurRadius ()Ljava/lang/Float; + public final fun getColor ()Ljava/lang/Integer; + public final fun getInset ()Ljava/lang/Boolean; + public final fun getOffsetX ()F + public final fun getOffsetY ()F + public final fun getSpreadRadius ()Ljava/lang/Float; + public fun hashCode ()I + public static final fun parse (Lcom/facebook/react/bridge/ReadableMap;)Lcom/facebook/react/uimanager/style/BoxShadow; + public fun toString ()Ljava/lang/String; +} + +public final class com/facebook/react/uimanager/style/BoxShadow$Companion { + public final fun parse (Lcom/facebook/react/bridge/ReadableMap;)Lcom/facebook/react/uimanager/style/BoxShadow; +} + public final class com/facebook/react/uimanager/style/ComputedBorderRadius { public fun ()V public fun (FFFF)V @@ -5945,6 +6004,47 @@ public final class com/facebook/react/uimanager/style/ComputedBorderRadiusProp : public static fun values ()[Lcom/facebook/react/uimanager/style/ComputedBorderRadiusProp; } +public abstract class com/facebook/react/uimanager/style/LogicalEdge : java/lang/Enum { + public static final field ALL Lcom/facebook/react/uimanager/style/LogicalEdge; + public static final field BLOCK Lcom/facebook/react/uimanager/style/LogicalEdge; + public static final field BLOCK_END Lcom/facebook/react/uimanager/style/LogicalEdge; + public static final field BLOCK_START Lcom/facebook/react/uimanager/style/LogicalEdge; + public static final field BOTTOM Lcom/facebook/react/uimanager/style/LogicalEdge; + public static final field Companion Lcom/facebook/react/uimanager/style/LogicalEdge$Companion; + public static final field END Lcom/facebook/react/uimanager/style/LogicalEdge; + public static final field HORIZONTAL Lcom/facebook/react/uimanager/style/LogicalEdge; + public static final field LEFT Lcom/facebook/react/uimanager/style/LogicalEdge; + public static final field RIGHT Lcom/facebook/react/uimanager/style/LogicalEdge; + public static final field START Lcom/facebook/react/uimanager/style/LogicalEdge; + public static final field TOP Lcom/facebook/react/uimanager/style/LogicalEdge; + public static final field VERTICAL Lcom/facebook/react/uimanager/style/LogicalEdge; + public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public static final fun fromSpacingType (I)Lcom/facebook/react/uimanager/style/LogicalEdge; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public abstract fun toSpacingType ()I + public static fun valueOf (Ljava/lang/String;)Lcom/facebook/react/uimanager/style/LogicalEdge; + public static fun values ()[Lcom/facebook/react/uimanager/style/LogicalEdge; +} + +public final class com/facebook/react/uimanager/style/LogicalEdge$Companion { + public final fun fromSpacingType (I)Lcom/facebook/react/uimanager/style/LogicalEdge; +} + +public final class com/facebook/react/uimanager/style/Overflow : java/lang/Enum { + public static final field Companion Lcom/facebook/react/uimanager/style/Overflow$Companion; + public static final field HIDDEN Lcom/facebook/react/uimanager/style/Overflow; + public static final field SCROLL Lcom/facebook/react/uimanager/style/Overflow; + public static final field VISIBLE Lcom/facebook/react/uimanager/style/Overflow; + public static final fun fromString (Ljava/lang/String;)Lcom/facebook/react/uimanager/style/Overflow; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/facebook/react/uimanager/style/Overflow; + public static fun values ()[Lcom/facebook/react/uimanager/style/Overflow; +} + +public final class com/facebook/react/uimanager/style/Overflow$Companion { + public final fun fromString (Ljava/lang/String;)Lcom/facebook/react/uimanager/style/Overflow; +} + public class com/facebook/react/uimanager/util/ReactFindViewUtil { public fun ()V public static fun addViewListener (Lcom/facebook/react/uimanager/util/ReactFindViewUtil$OnViewFoundListener;)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt new file mode 100644 index 000000000000..0ff08bc70802 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt @@ -0,0 +1,184 @@ +/* + * 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.Canvas +import android.graphics.Color +import android.graphics.Rect +import android.view.View +import androidx.annotation.ColorInt +import androidx.annotation.RequiresApi +import com.facebook.react.common.annotations.UnstableReactNativeAPI +import com.facebook.react.uimanager.drawable.CSSBackgroundDrawable +import com.facebook.react.uimanager.drawable.CompositeBackgroundDrawable +import com.facebook.react.uimanager.drawable.InsetBoxShadowDrawable +import com.facebook.react.uimanager.drawable.OutsetBoxShadowDrawable +import com.facebook.react.uimanager.style.BorderRadiusProp +import com.facebook.react.uimanager.style.BorderStyle +import com.facebook.react.uimanager.style.BoxShadow +import com.facebook.react.uimanager.style.LogicalEdge + +/** + * BackgroundStyleApplicator is responsible for applying backgrounds, borders, and related effects, + * to an Android view + */ +@OptIn(UnstableReactNativeAPI::class) +public object BackgroundStyleApplicator { + + @JvmStatic + public fun setBackgroundColor(view: View, @ColorInt color: Int?): Unit { + // No color to set, and no color already set + if ((color == null || color == Color.TRANSPARENT) && + view.background !is CompositeBackgroundDrawable) { + return + } + + ensureCSSBackground(view).color = color ?: Color.TRANSPARENT + } + + @JvmStatic + @ColorInt + public fun getBackgroundColor(view: View): Int? = getCSSBackground(view)?.color + + @JvmStatic + public fun setBorderWidth(view: View, edge: LogicalEdge, width: Float?): Unit = + ensureCSSBackground(view) + .setBorderWidth(edge.toSpacingType(), PixelUtil.toPixelFromDIP(width ?: Float.NaN)) + + @JvmStatic + public fun getBorderWidth(view: View, edge: LogicalEdge): Float? { + val width = getCSSBackground(view)?.getBorderWidth(edge.toSpacingType()) + return if (width == null || width.isNaN()) null else PixelUtil.toDIPFromPixel((width)) + } + + @JvmStatic + public fun setBorderColor(view: View, edge: LogicalEdge, @ColorInt color: Int?): Unit = + ensureCSSBackground(view).setBorderColor(edge.toSpacingType(), color) + + @JvmStatic + @ColorInt + public fun getBorderColor(view: View, edge: LogicalEdge): Int? = + getCSSBackground(view)?.getBorderColor(edge.toSpacingType()) + + @JvmStatic + public fun setBorderRadius( + view: View, + corner: BorderRadiusProp, + // TODO: LengthPercentage silently converts from pixels to DIPs before here already + radius: LengthPercentage? + ): Unit = ensureCSSBackground(view).setBorderRadius(corner, radius) + + @JvmStatic + public fun getBorderRadius(view: View, corner: BorderRadiusProp): LengthPercentage? = + getCSSBackground(view)?.borderRadius?.get(corner) + + @JvmStatic + public fun setBorderStyle(view: View, borderStyle: BorderStyle?): Unit { + ensureCSSBackground(view).borderStyle = borderStyle + } + + @JvmStatic + public fun getBorderStyle(view: View): BorderStyle? = getCSSBackground(view)?.borderStyle + + @JvmStatic + @RequiresApi(31) + public fun setShadows(view: View, shadows: List): Unit { + val shadowDrawables = + shadows.map { boxShadow -> + val offsetX = boxShadow.offsetX + val offsetY = boxShadow.offsetY + val color = boxShadow.color ?: Color.TRANSPARENT + val blurRadius = boxShadow.blurRadius ?: 0f + val spreadRadius = boxShadow.spreadRadius ?: 0f + val inset = boxShadow.inset ?: false + + if (inset) + InsetBoxShadowDrawable( + view.context, + ensureCSSBackground(view), + color, + offsetX, + offsetY, + blurRadius, + spreadRadius) + else + OutsetBoxShadowDrawable( + view.context, + ensureCSSBackground(view), + color, + offsetX, + offsetY, + blurRadius, + spreadRadius) + } + + updateCompositeDrawable( + view, + ensureCompositeBackgroundDrawable(view).withNewShadows(shadowDrawables.toTypedArray())) + } + + @JvmStatic + public fun clipToPaddingBox(view: View, canvas: Canvas): Unit { + // The canvas may be scrolled, so we need to offset + val drawingRect = Rect() + view.getDrawingRect(drawingRect) + + val cssBackground = getCSSBackground(view) + if (cssBackground == null) { + canvas.clipRect(drawingRect) + return + } + + val paddingBoxPath = cssBackground.paddingBoxPath + if (paddingBoxPath != null) { + paddingBoxPath.offset(drawingRect.left.toFloat(), drawingRect.top.toFloat()) + canvas.clipPath(paddingBoxPath) + } else { + val paddingBoxRect = cssBackground.paddingBoxRect + paddingBoxRect.offset(drawingRect.left.toFloat(), drawingRect.top.toFloat()) + canvas.clipRect(paddingBoxRect) + } + } + + private fun updateCompositeDrawable( + view: View, + compositeDrawable: CompositeBackgroundDrawable + ): Unit { + view.background = null + view.background = compositeDrawable + view.invalidate() + } + + private fun ensureCompositeBackgroundDrawable(view: View): CompositeBackgroundDrawable { + if (view.background is CompositeBackgroundDrawable) { + return view.background as CompositeBackgroundDrawable + } + + val compositeDrawable = CompositeBackgroundDrawable(view.background, null, emptyArray(), null) + updateCompositeDrawable(view, compositeDrawable) + return compositeDrawable + } + + private fun ensureCSSBackground(view: View): CSSBackgroundDrawable { + val compositeBackgroundDrawable = ensureCompositeBackgroundDrawable(view) + if (compositeBackgroundDrawable.cssBackground != null) { + return compositeBackgroundDrawable.cssBackground + } else { + val cssBackground = CSSBackgroundDrawable(view.context) + updateCompositeDrawable(view, compositeBackgroundDrawable.withNewCssBackground(cssBackground)) + return cssBackground + } + } + + private fun getCSSBackground(view: View): CSSBackgroundDrawable? { + if (view.background is CompositeBackgroundDrawable) { + return (view.background as CompositeBackgroundDrawable).cssBackground + } + return null + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/PixelUtil.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/PixelUtil.kt index 48f1f1e2f246..7009a586adc7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/PixelUtil.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/PixelUtil.kt @@ -15,6 +15,10 @@ public object PixelUtil { /** Convert from DIP to PX */ @JvmStatic public fun toPixelFromDIP(value: Float): Float { + if (value.isNaN()) { + return Float.NaN + } + return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, value, DisplayMetricsHolder.getWindowDisplayMetrics()) } @@ -22,6 +26,10 @@ public object PixelUtil { /** Convert from DIP to PX */ @JvmStatic public fun toPixelFromDIP(value: Double): Float { + if (value.isNaN()) { + return Float.NaN + } + return toPixelFromDIP(value.toFloat()) } @@ -29,6 +37,10 @@ public object PixelUtil { @JvmOverloads @JvmStatic public fun toPixelFromSP(value: Float, maxFontScale: Float = Float.NaN): Float { + if (value.isNaN()) { + return Float.NaN + } + val displayMetrics = DisplayMetricsHolder.getWindowDisplayMetrics() val scaledValue = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, value, displayMetrics) @@ -42,12 +54,20 @@ public object PixelUtil { /** Convert from SP to PX */ @JvmStatic public fun toPixelFromSP(value: Double): Float { + if (value.isNaN()) { + return Float.NaN + } + return toPixelFromSP(value.toFloat()) } /** Convert from PX to DP */ @JvmStatic public fun toDIPFromPixel(value: Float): Float { + if (value.isNaN()) { + return Float.NaN + } + return value / DisplayMetricsHolder.getWindowDisplayMetrics().density } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CSSBackgroundDrawable.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CSSBackgroundDrawable.java index 7369bc9de442..844f4d4f72f4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CSSBackgroundDrawable.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CSSBackgroundDrawable.java @@ -36,6 +36,7 @@ import com.facebook.react.uimanager.Spacing; import com.facebook.react.uimanager.style.BorderRadiusProp; import com.facebook.react.uimanager.style.BorderRadiusStyle; +import com.facebook.react.uimanager.style.BorderStyle; import com.facebook.react.uimanager.style.ComputedBorderRadius; import java.util.Locale; import java.util.Objects; @@ -64,29 +65,23 @@ public class CSSBackgroundDrawable extends Drawable { // 0 == 0x00000000, all bits set to 0. private static final int ALL_BITS_UNSET = 0; - private enum BorderStyle { - SOLID, - DASHED, - DOTTED; + private static @Nullable PathEffect getPathEffect(BorderStyle style, float borderWidth) { + switch (style) { + case SOLID: + return null; - public static @Nullable PathEffect getPathEffect(BorderStyle style, float borderWidth) { - switch (style) { - case SOLID: - return null; + case DASHED: + return new DashPathEffect( + new float[] {borderWidth * 3, borderWidth * 3, borderWidth * 3, borderWidth * 3}, 0); - case DASHED: - return new DashPathEffect( - new float[] {borderWidth * 3, borderWidth * 3, borderWidth * 3, borderWidth * 3}, 0); + case DOTTED: + return new DashPathEffect( + new float[] {borderWidth, borderWidth, borderWidth, borderWidth}, 0); - case DOTTED: - return new DashPathEffect( - new float[] {borderWidth, borderWidth, borderWidth, borderWidth}, 0); - - default: - return null; - } + default: + return null; } - }; + } /* Value at Spacing.ALL index used for rounded borders, whole array used by rectangular borders */ private @Nullable Spacing mBorderWidth; @@ -228,6 +223,7 @@ public void setBorderColor(int position, @Nullable Integer color) { this.setBorderRGB(position, rgbComponent); this.setBorderAlpha(position, alphaComponent); mNeedUpdatePathForBorderRadius = true; + invalidateSelf(); } private void setBorderRGB(int position, float rgb) { @@ -255,6 +251,10 @@ private void setBorderAlpha(int position, float alpha) { public void setBorderStyle(@Nullable String style) { BorderStyle borderStyle = style == null ? null : BorderStyle.valueOf(style.toUpperCase(Locale.US)); + setBorderStyle(borderStyle); + } + + public void setBorderStyle(@Nullable BorderStyle borderStyle) { if (mBorderStyle != borderStyle) { mBorderStyle = borderStyle; mNeedUpdatePathForBorderRadius = true; @@ -262,6 +262,10 @@ public void setBorderStyle(@Nullable String style) { } } + public @Nullable BorderStyle getBorderStyle() { + return mBorderStyle; + } + /** * @deprecated Use {@link #setBorderRadius(BorderRadiusProp, LengthPercentage)} instead. */ @@ -286,6 +290,7 @@ public void setRadius(float radius, int position) { if (boxedRadius == null) { mBorderRadius.set(BorderRadiusProp.values()[position], null); + invalidateSelf(); } else { setBorderRadius( BorderRadiusProp.values()[position], @@ -1012,14 +1017,23 @@ private static void getEllipseIntersectionWithLine( } public float getBorderWidthOrDefaultTo(final float defaultValue, final int spacingType) { - if (mBorderWidth == null) { + @Nullable Float width = getBorderWidth(spacingType); + if (width == null) { return defaultValue; } + return width; + } + + public @Nullable Float getBorderWidth(int spacingType) { + if (mBorderWidth == null) { + return null; + } + final float width = mBorderWidth.getRaw(spacingType); if (Float.isNaN(width)) { - return defaultValue; + return null; } return width; @@ -1029,7 +1043,7 @@ public float getBorderWidthOrDefaultTo(final float defaultValue, final int spaci private void updatePathEffect() { // Used for rounded border and rounded background PathEffect mPathEffectForBorderStyle = - mBorderStyle != null ? BorderStyle.getPathEffect(mBorderStyle, getFullBorderWidth()) : null; + mBorderStyle != null ? getPathEffect(mBorderStyle, getFullBorderWidth()) : null; mPaint.setPathEffect(mPathEffectForBorderStyle); } @@ -1037,7 +1051,7 @@ private void updatePathEffect() { private void updatePathEffect(int borderWidth) { PathEffect pathEffectForBorderStyle = null; if (mBorderStyle != null) { - pathEffectForBorderStyle = BorderStyle.getPathEffect(mBorderStyle, borderWidth); + pathEffectForBorderStyle = getPathEffect(mBorderStyle, borderWidth); } mPaint.setPathEffect(pathEffectForBorderStyle); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CompositeBackgroundDrawable.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CompositeBackgroundDrawable.kt new file mode 100644 index 000000000000..b7f20a8faaf0 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CompositeBackgroundDrawable.kt @@ -0,0 +1,63 @@ +/* + * 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.drawable + +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import com.facebook.react.common.annotations.UnstableReactNativeAPI + +/** + * CompositeBackgroundDrawable can overlay multiple different layers, shadows, and native effects + * such as ripple, into an Android View's background drawable. + */ +@OptIn(UnstableReactNativeAPI::class) +internal class CompositeBackgroundDrawable( + /** + * Any non-react-managed background already part of the view, like one set as Android style on a + * TextInput + */ + public val originalBackground: Drawable? = null, + + /** + * CSS background layer and border rendering + * + * TODO: we should extract path logic from here, and fast-path to using simpler drawables like + * ColorDrawable in the common cases + */ + public val cssBackground: CSSBackgroundDrawable? = null, + + /** Inner and outer box shadows */ + public val shadows: Array = emptyArray(), + + /** Native riplple effect (e.g. used by TouchableNativeFeedback) */ + public val nativeRipple: Drawable? = null +) : + LayerDrawable( + listOfNotNull(originalBackground, cssBackground, *shadows, nativeRipple).toTypedArray()) { + + init { + // We want to overlay drawables, instead of placing future drawables within the content area of + // previous ones. E.g. an EditText style may set padding on a TextInput, but we don't want to + // constrain background color to the area inside of the padding. + setPaddingMode(LayerDrawable.PADDING_MODE_STACK) + } + + public fun withNewCssBackground( + cssBackground: CSSBackgroundDrawable? + ): CompositeBackgroundDrawable { + return CompositeBackgroundDrawable(originalBackground, cssBackground, shadows, nativeRipple) + } + + public fun withNewShadows(newShadows: Array): CompositeBackgroundDrawable { + return CompositeBackgroundDrawable(originalBackground, cssBackground, newShadows, nativeRipple) + } + + public fun withNewNativeRipple(newRipple: Drawable?): CompositeBackgroundDrawable { + return CompositeBackgroundDrawable(originalBackground, cssBackground, shadows, newRipple) + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BorderStyle.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BorderStyle.kt new file mode 100644 index 000000000000..c23030b3fc90 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BorderStyle.kt @@ -0,0 +1,26 @@ +/* + * 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.style + +public enum class BorderStyle { + SOLID, + DASHED, + DOTTED; + + public companion object { + @JvmStatic + public fun fromString(borderStyle: String): BorderStyle? { + return when (borderStyle.lowercase()) { + "solid" -> SOLID + "dashed" -> DASHED + "dotted" -> DOTTED + else -> null + } + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BoxShadow.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BoxShadow.kt new file mode 100644 index 000000000000..2a24859e98a9 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BoxShadow.kt @@ -0,0 +1,50 @@ +/* + * 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.style + +import androidx.annotation.ColorInt +import com.facebook.react.bridge.ReadableMap + +/** Represents all logical properties and shorthands for border radius. */ +public data class BoxShadow( + val offsetX: Float, + val offsetY: Float, + @ColorInt val color: Int? = null, + val blurRadius: Float? = null, + val spreadRadius: Float? = null, + val inset: Boolean? = null, +) { + public companion object { + @JvmStatic + public fun parse(boxShadow: ReadableMap): BoxShadow? { + if (!(boxShadow.hasKey("offsetX") && boxShadow.hasKey("offsetY"))) { + return null + } + + val offsetX = boxShadow.getDouble("offsetX").toFloat() + val offsetY = boxShadow.getDouble("offsetY").toFloat() + + val color = if (boxShadow.hasKey("color")) boxShadow.getInt("color") else null + val blurRadius = + if (boxShadow.hasKey("blurRadius")) boxShadow.getDouble("blurRadius").toFloat() else null + val spreadRadius = + if (boxShadow.hasKey("spreadRadius")) boxShadow.getDouble("spreadRadius").toFloat() + else null + val inset = if (boxShadow.hasKey("inset")) boxShadow.getBoolean("inset") else null + + return BoxShadow( + offsetX = offsetX, + offsetY = offsetY, + color = color, + blurRadius = blurRadius, + spreadRadius = spreadRadius, + inset = inset, + ) + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/LogicalEdge.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/LogicalEdge.kt new file mode 100644 index 000000000000..1f35909d6f00 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/LogicalEdge.kt @@ -0,0 +1,79 @@ +/* + * 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.style + +import com.facebook.react.uimanager.Spacing +import java.lang.IllegalArgumentException + +/** Represents the collection of possible box edges and shorthands. */ +public enum class LogicalEdge { + ALL { + override fun toSpacingType(): Int = Spacing.ALL + }, + LEFT { + override fun toSpacingType(): Int = Spacing.LEFT + }, + RIGHT { + override fun toSpacingType(): Int = Spacing.RIGHT + }, + TOP { + override fun toSpacingType(): Int = Spacing.TOP + }, + BOTTOM { + override fun toSpacingType(): Int = Spacing.BOTTOM + }, + START { + override fun toSpacingType(): Int = Spacing.START + }, + END { + override fun toSpacingType(): Int = Spacing.END + }, + HORIZONTAL { + override fun toSpacingType(): Int = Spacing.HORIZONTAL + }, + VERTICAL { + override fun toSpacingType(): Int = Spacing.VERTICAL + }, + BLOCK_START { + override fun toSpacingType(): Int = Spacing.BLOCK_START + }, + BLOCK_END { + override fun toSpacingType(): Int = Spacing.BLOCK_END + }, + BLOCK { + override fun toSpacingType(): Int = Spacing.BLOCK + }; + + // TODO: not supported by Spacing users + // INLINE_START, + // INLINE_END, + // INLINE; + + abstract public fun toSpacingType(): Int + + public companion object { + @JvmStatic + public fun fromSpacingType(spacingType: Int): LogicalEdge { + return when (spacingType) { + Spacing.ALL -> ALL + Spacing.LEFT -> LEFT + Spacing.RIGHT -> RIGHT + Spacing.TOP -> TOP + Spacing.BOTTOM -> BOTTOM + Spacing.START -> START + Spacing.END -> END + Spacing.HORIZONTAL -> HORIZONTAL + Spacing.VERTICAL -> VERTICAL + Spacing.BLOCK_START -> BLOCK_START + Spacing.BLOCK_END -> BLOCK_END + Spacing.BLOCK -> BLOCK + else -> throw IllegalArgumentException("Unknown spacing type: $spacingType") + } + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/Overflow.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/Overflow.kt new file mode 100644 index 000000000000..a057a79b144d --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/Overflow.kt @@ -0,0 +1,26 @@ +/* + * 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.style + +public enum class Overflow { + VISIBLE, + HIDDEN, + SCROLL; + + public companion object { + @JvmStatic + public fun fromString(overflow: String): Overflow { + return when (overflow.lowercase()) { + "visible" -> VISIBLE + "hidden" -> HIDDEN + "scroll" -> SCROLL + else -> throw IllegalArgumentException("Unknown overflow: $overflow") + } + } + } +} diff --git a/packages/react-native/types/experimental.d.ts b/packages/react-native/types/experimental.d.ts index deea3034b457..859252932a97 100644 --- a/packages/react-native/types/experimental.d.ts +++ b/packages/react-native/types/experimental.d.ts @@ -149,8 +149,11 @@ declare module '.' { } export interface ViewStyle { - experimental_boxShadow?: BoxShadowPrimitive | undefined; - experimental_filter?: ReadonlyArray | undefined; + experimental_boxShadow?: + | ReadonlyArray + | string + | undefined; + experimental_filter?: ReadonlyArray | string | undefined; experimental_mixBlendMode?: BlendMode | undefined; } }