diff --git a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js index df1ed4cd5d9..2bfc2ae19f2 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -174,6 +174,10 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { borderTopStartRadius: true, cursor: true, opacity: true, + outlineColor: colorAttributes, + outlineOffset: true, + outlineStyle: true, + outlineWidth: true, pointerEvents: true, /** diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js index 70765523f98..3c73549c1a2 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js @@ -268,6 +268,11 @@ const validAttributesForNonEventProps = { borderLeftWidth: true, borderRightWidth: true, + outlineColor: {process: require('../StyleSheet/processColor').default}, + outlineOffset: true, + outlineStyle: true, + outlineWidth: true, + start: true, end: true, left: true, diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts index 62f52a7db03..9226d25e395 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts @@ -411,6 +411,10 @@ export interface ViewStyle extends FlexStyle, ShadowStyleIOS, TransformsStyle { borderTopLeftRadius?: AnimatableNumericValue | string | undefined; borderTopRightRadius?: AnimatableNumericValue | string | undefined; borderTopStartRadius?: AnimatableNumericValue | string | undefined; + outlineColor?: ColorValue | undefined; + outlineOffset?: AnimatableNumericValue | undefined; + outlineStyle?: 'solid' | 'dotted' | 'dashed' | undefined; + outlineWidth?: AnimatableNumericValue | undefined; opacity?: AnimatableNumericValue | undefined; /** * Sets the elevation of a view, using Android's underlying diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js index 4be9ae684ba..83b3d12ba76 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js @@ -785,6 +785,10 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{ borderStartWidth?: AnimatableNumericValue, borderTopWidth?: AnimatableNumericValue, opacity?: AnimatableNumericValue, + outlineColor?: ____ColorValue_Internal, + outlineOffset?: AnimatableNumericValue, + outlineStyle?: 'solid' | 'dotted' | 'dashed', + outlineWidth?: AnimatableNumericValue, elevation?: number, pointerEvents?: 'auto' | 'none' | 'box-none' | 'box-only', cursor?: CursorValue, 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 d2e35fcce82..00b85b64cbe 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 @@ -8363,6 +8363,10 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{ borderStartWidth?: AnimatableNumericValue, borderTopWidth?: AnimatableNumericValue, opacity?: AnimatableNumericValue, + outlineColor?: ____ColorValue_Internal, + outlineOffset?: AnimatableNumericValue, + outlineStyle?: \\"solid\\" | \\"dotted\\" | \\"dashed\\", + outlineWidth?: AnimatableNumericValue, elevation?: number, pointerEvents?: \\"auto\\" | \\"none\\" | \\"box-none\\" | \\"box-only\\", cursor?: CursorValue, diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index c7f223a1353..539d1e65255 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -4128,6 +4128,10 @@ public final class com/facebook/react/uimanager/BackgroundStyleApplicator { 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 getOutlineColor (Landroid/view/View;)Ljava/lang/Integer; + public final fun getOutlineOffset (Landroid/view/View;)Ljava/lang/Float; + public final fun getOutlineStyle (Landroid/view/View;)Lcom/facebook/react/uimanager/style/OutlineStyle; + public final fun getOutlineWidth (Landroid/view/View;)Ljava/lang/Float; public static final fun reset (Landroid/view/View;)V public static final fun setBackgroundColor (Landroid/view/View;Ljava/lang/Integer;)V public static final fun setBackgroundImage (Landroid/view/View;Ljava/util/List;)V @@ -4138,6 +4142,10 @@ public final class com/facebook/react/uimanager/BackgroundStyleApplicator { public static final fun setBoxShadow (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;)V public static final fun setBoxShadow (Landroid/view/View;Ljava/util/List;)V public static final fun setFeedbackUnderlay (Landroid/view/View;Landroid/graphics/drawable/Drawable;)V + public static final fun setOutlineColor (Landroid/view/View;Ljava/lang/Integer;)V + public static final fun setOutlineOffset (Landroid/view/View;F)V + public static final fun setOutlineStyle (Landroid/view/View;Lcom/facebook/react/uimanager/style/OutlineStyle;)V + public static final fun setOutlineWidth (Landroid/view/View;F)V } public abstract class com/facebook/react/uimanager/BaseViewManager : com/facebook/react/uimanager/ViewManager, android/view/View$OnLayoutChangeListener, com/facebook/react/uimanager/BaseViewManagerInterface { @@ -4173,6 +4181,10 @@ public abstract class com/facebook/react/uimanager/BaseViewManager : com/faceboo public fun setMoveShouldSetResponderCapture (Landroid/view/View;Z)V public fun setNativeId (Landroid/view/View;Ljava/lang/String;)V public fun setOpacity (Landroid/view/View;F)V + public fun setOutlineColor (Landroid/view/View;Ljava/lang/Integer;)V + public fun setOutlineOffset (Landroid/view/View;F)V + public fun setOutlineStyle (Landroid/view/View;Ljava/lang/String;)V + public fun setOutlineWidth (Landroid/view/View;F)V public fun setPointerEnter (Landroid/view/View;Z)V public fun setPointerEnterCapture (Landroid/view/View;Z)V public fun setPointerLeave (Landroid/view/View;Z)V @@ -5645,6 +5657,10 @@ public final class com/facebook/react/uimanager/ViewProps { public static final field ON Ljava/lang/String; public static final field ON_LAYOUT Ljava/lang/String; public static final field OPACITY Ljava/lang/String; + public static final field OUTLINE_COLOR Ljava/lang/String; + public static final field OUTLINE_OFFSET Ljava/lang/String; + public static final field OUTLINE_STYLE Ljava/lang/String; + public static final field OUTLINE_WIDTH Ljava/lang/String; public static final field OVERFLOW Ljava/lang/String; public static final field PADDING Ljava/lang/String; public static final field PADDING_BOTTOM Ljava/lang/String; @@ -6235,6 +6251,21 @@ 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/OutlineStyle : java/lang/Enum { + public static final field Companion Lcom/facebook/react/uimanager/style/OutlineStyle$Companion; + public static final field DASHED Lcom/facebook/react/uimanager/style/OutlineStyle; + public static final field DOTTED Lcom/facebook/react/uimanager/style/OutlineStyle; + public static final field SOLID Lcom/facebook/react/uimanager/style/OutlineStyle; + public static final fun fromString (Ljava/lang/String;)Lcom/facebook/react/uimanager/style/OutlineStyle; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/facebook/react/uimanager/style/OutlineStyle; + public static fun values ()[Lcom/facebook/react/uimanager/style/OutlineStyle; +} + +public final class com/facebook/react/uimanager/style/OutlineStyle$Companion { + public final fun fromString (Ljava/lang/String;)Lcom/facebook/react/uimanager/style/OutlineStyle; +} + 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; 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 index d6368fd67ec..21bb6cad0a7 100644 --- 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 @@ -25,6 +25,7 @@ import com.facebook.react.uimanager.drawable.CompositeBackgroundDrawable import com.facebook.react.uimanager.drawable.InsetBoxShadowDrawable import com.facebook.react.uimanager.drawable.MIN_INSET_BOX_SHADOW_SDK_VERSION import com.facebook.react.uimanager.drawable.MIN_OUTSET_BOX_SHADOW_SDK_VERSION +import com.facebook.react.uimanager.drawable.OutlineDrawable import com.facebook.react.uimanager.drawable.OutsetBoxShadowDrawable import com.facebook.react.uimanager.style.BackgroundImageLayer import com.facebook.react.uimanager.style.BorderInsets @@ -33,6 +34,7 @@ import com.facebook.react.uimanager.style.BorderRadiusStyle import com.facebook.react.uimanager.style.BorderStyle import com.facebook.react.uimanager.style.BoxShadow import com.facebook.react.uimanager.style.LogicalEdge +import com.facebook.react.uimanager.style.OutlineStyle /** * BackgroundStyleApplicator is responsible for applying backgrounds, borders, and related effects, @@ -123,6 +125,13 @@ public object BackgroundStyleApplicator { } } } + + val outline = compositeBackgroundDrawable.outline + if (outline != null) { + outline.borderRadius = outline.borderRadius ?: BorderRadiusStyle() + outline.borderRadius?.set(corner, radius) + outline.invalidateSelf() + } } @JvmStatic @@ -137,6 +146,42 @@ public object BackgroundStyleApplicator { @JvmStatic public fun getBorderStyle(view: View): BorderStyle? = getCSSBackground(view)?.borderStyle + @JvmStatic + public fun setOutlineColor(view: View, @ColorInt outlineColor: Int?): Unit { + val outline = ensureOutlineDrawable(view) + if (outlineColor != null) { + outline.outlineColor = outlineColor + } + } + + @JvmStatic public fun getOutlineColor(view: View): Int? = getOutlineDrawable(view)?.outlineColor + + @JvmStatic + public fun setOutlineOffset(view: View, outlineOffset: Float): Unit { + val outline = ensureOutlineDrawable(view) + outline.outlineOffset = outlineOffset.dpToPx() + } + + public fun getOutlineOffset(view: View): Float? = getOutlineDrawable(view)?.outlineOffset + + @JvmStatic + public fun setOutlineStyle(view: View, outlineStyle: OutlineStyle?): Unit { + val outline = ensureOutlineDrawable(view) + if (outlineStyle != null) { + outline.outlineStyle = outlineStyle + } + } + + public fun getOutlineStyle(view: View): OutlineStyle? = getOutlineDrawable(view)?.outlineStyle + + @JvmStatic + public fun setOutlineWidth(view: View, width: Float): Unit { + val outline = ensureOutlineDrawable(view) + outline.outlineWidth = width.dpToPx() + } + + public fun getOutlineWidth(view: View): Float? = getOutlineDrawable(view)?.outlineOffset + @JvmStatic public fun setBoxShadow(view: View, shadows: List): Unit { if (ViewUtil.getUIManagerType(view) != UIManagerType.FABRIC) { @@ -260,4 +305,26 @@ public object BackgroundStyleApplicator { private fun getCSSBackground(view: View): CSSBackgroundDrawable? = getCompositeBackgroundDrawable(view)?.cssBackground + + private fun ensureOutlineDrawable(view: View): OutlineDrawable { + val compositeBackgroundDrawable = ensureCompositeBackgroundDrawable(view) + var outline = compositeBackgroundDrawable.outline + if (outline == null) { + outline = + OutlineDrawable( + context = view.context, + borderRadius = ensureCSSBackground(view).borderRadius.copy(), + outlineColor = Color.BLACK, + outlineOffset = 0f, + outlineStyle = OutlineStyle.SOLID, + outlineWidth = 0f, + ) + view.background = compositeBackgroundDrawable.withNewOutline(outline) + } + + return outline + } + + private fun getOutlineDrawable(view: View): OutlineDrawable? = + getCompositeBackgroundDrawable(view)?.outline } 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 d9a79ccf2b1..4c9df40644a 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 @@ -27,12 +27,14 @@ import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.bridge.ReadableType; import com.facebook.react.common.MapBuilder; import com.facebook.react.common.ReactConstants; +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole; import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role; import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.common.UIManagerType; import com.facebook.react.uimanager.common.ViewUtil; import com.facebook.react.uimanager.events.PointerEventHelper; +import com.facebook.react.uimanager.style.OutlineStyle; import com.facebook.react.uimanager.util.ReactFindViewUtil; import java.util.ArrayList; import java.util.HashMap; @@ -780,6 +782,37 @@ public abstract class BaseViewManager = emptyList(), + + /** Outline */ + public val outline: OutlineDrawable? = null, ) : LayerDrawable( listOfNotNull( @@ -50,7 +53,8 @@ internal class CompositeBackgroundDrawable( *outerShadows.asReversed().toTypedArray(), cssBackground, feedbackUnderlay, - *innerShadows.asReversed().toTypedArray()) + *innerShadows.asReversed().toTypedArray(), + outline) .toTypedArray()) { // Holder value for currently set insets @@ -67,7 +71,7 @@ internal class CompositeBackgroundDrawable( cssBackground: CSSBackgroundDrawable? ): CompositeBackgroundDrawable { return CompositeBackgroundDrawable( - originalBackground, outerShadows, cssBackground, feedbackUnderlay, innerShadows) + originalBackground, outerShadows, cssBackground, feedbackUnderlay, innerShadows, outline) } public fun withNewShadows( @@ -75,11 +79,16 @@ internal class CompositeBackgroundDrawable( innerShadows: List ): CompositeBackgroundDrawable { return CompositeBackgroundDrawable( - originalBackground, outerShadows, cssBackground, feedbackUnderlay, innerShadows) + originalBackground, outerShadows, cssBackground, feedbackUnderlay, innerShadows, outline) + } + + public fun withNewOutline(outline: OutlineDrawable): CompositeBackgroundDrawable { + return CompositeBackgroundDrawable( + originalBackground, outerShadows, cssBackground, feedbackUnderlay, innerShadows, outline) } public fun withNewFeedbackUnderlay(newUnderlay: Drawable?): CompositeBackgroundDrawable { return CompositeBackgroundDrawable( - originalBackground, outerShadows, cssBackground, newUnderlay, innerShadows) + originalBackground, outerShadows, cssBackground, newUnderlay, innerShadows, outline) } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/OutlineDrawable.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/OutlineDrawable.kt new file mode 100644 index 00000000000..e23562bc539 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/OutlineDrawable.kt @@ -0,0 +1,182 @@ +/* + * 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.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.DashPathEffect +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PathEffect +import android.graphics.RectF +import android.graphics.drawable.Drawable +import com.facebook.react.uimanager.PixelUtil.dpToPx +import com.facebook.react.uimanager.style.BorderRadiusStyle +import com.facebook.react.uimanager.style.ComputedBorderRadius +import com.facebook.react.uimanager.style.CornerRadii +import com.facebook.react.uimanager.style.OutlineStyle +import kotlin.math.roundToInt +import kotlin.properties.ObservableProperty +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/** Draws outline https://drafts.csswg.org/css-ui/#outline */ +internal class OutlineDrawable( + private val context: Context, + borderRadius: BorderRadiusStyle? = null, + outlineColor: Int, + outlineOffset: Float, + outlineStyle: OutlineStyle, + outlineWidth: Float, +) : Drawable() { + /** + * There is a small gap between the edges of adjacent paths, such as between its Border and its + * Outline. The smallest amount (found to be 0.8f) is used to shrink outline's path, overlapping + * them and closing the visible gap. + */ + private val gapBetweenPaths = 0.8f + + private fun invalidatingChange(initialValue: T): ReadWriteProperty = + object : ObservableProperty(initialValue) { + override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) { + if (oldValue != newValue) { + invalidateSelf() + } + } + } + + public var borderRadius: BorderRadiusStyle? by invalidatingChange(borderRadius) + public var outlineOffset: Float by invalidatingChange(outlineOffset) + public var outlineStyle: OutlineStyle = outlineStyle + set(value) { + if (value != field) { + field = value + outlinePaint.pathEffect = getPathEffect(value, outlineWidth) + invalidateSelf() + } + } + + public var outlineColor: Int = outlineColor + set(value) { + if (value != field) { + field = value + outlinePaint.color = value + invalidateSelf() + } + } + + public var outlineWidth: Float = outlineWidth + set(value) { + if (value != field) { + field = value + outlinePaint.strokeWidth = value + outlinePaint.pathEffect = getPathEffect(outlineStyle, value) + invalidateSelf() + } + } + + private val outlinePaint: Paint = + Paint().apply { + style = Paint.Style.STROKE + color = outlineColor + strokeWidth = outlineWidth + pathEffect = getPathEffect(outlineStyle, outlineWidth) + } + + private var computedBorderRadius: ComputedBorderRadius? = null + private var tempRectForOutline = RectF() + + private val pathForOutline = Path() + + override fun setAlpha(alpha: Int) { + outlinePaint.alpha = (((alpha / 255f) * (Color.alpha(outlineColor) / 255f)) * 255f).roundToInt() + invalidateSelf() + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + outlinePaint.colorFilter = colorFilter + invalidateSelf() + } + + override fun getOpacity(): Int = + ((outlinePaint.alpha / 255f) / (Color.alpha(outlineColor) / 255f) * 255f).roundToInt() + + override fun draw(canvas: Canvas) { + pathForOutline.reset() + + computedBorderRadius = + borderRadius?.resolve( + layoutDirection, + context, + bounds.width().toFloat().dpToPx(), + bounds.height().toFloat().dpToPx()) + + updateOutlineRect() + if (computedBorderRadius != null && computedBorderRadius?.hasRoundedBorders() == true) { + drawRoundedOutline(canvas) + } else { + drawRectangularOutline(canvas) + } + } + + private fun updateOutlineRect() { + tempRectForOutline.set(bounds) + + tempRectForOutline.top -= outlineWidth * 0.5f + outlineOffset - gapBetweenPaths + tempRectForOutline.bottom += outlineWidth * 0.5f + outlineOffset - gapBetweenPaths + tempRectForOutline.left -= outlineWidth * 0.5f + outlineOffset - gapBetweenPaths + tempRectForOutline.right += outlineWidth * 0.5f + outlineOffset - gapBetweenPaths + } + + private fun getPathEffect(style: OutlineStyle, outlineWidth: Float): PathEffect? { + return when (style) { + OutlineStyle.SOLID -> null + OutlineStyle.DASHED -> + DashPathEffect( + floatArrayOf(outlineWidth * 3, outlineWidth * 3, outlineWidth * 3, outlineWidth * 3), + 0f) + OutlineStyle.DOTTED -> + DashPathEffect(floatArrayOf(outlineWidth, outlineWidth, outlineWidth, outlineWidth), 0f) + } + } + + private fun calculateRadius(radius: Float, outlineWidth: Float) = + if (radius != 0f) radius + outlineWidth * 0.5f else 0f + + private fun drawRectangularOutline(canvas: Canvas) { + pathForOutline.addRect(tempRectForOutline, Path.Direction.CW) + + canvas.drawPath(pathForOutline, outlinePaint) + } + + private fun drawRoundedOutline(canvas: Canvas) { + val topLeftRadius = computedBorderRadius?.topLeft?.toPixelFromDIP() ?: CornerRadii(0f, 0f) + val topRightRadius = computedBorderRadius?.topRight?.toPixelFromDIP() ?: CornerRadii(0f, 0f) + val bottomLeftRadius = computedBorderRadius?.bottomLeft?.toPixelFromDIP() ?: CornerRadii(0f, 0f) + val bottomRightRadius = + computedBorderRadius?.bottomRight?.toPixelFromDIP() ?: CornerRadii(0f, 0f) + + pathForOutline.addRoundRect( + tempRectForOutline, + floatArrayOf( + calculateRadius(topLeftRadius.horizontal, outlineWidth), + calculateRadius(topLeftRadius.vertical, outlineWidth), + calculateRadius(topRightRadius.horizontal, outlineWidth), + calculateRadius(topRightRadius.vertical, outlineWidth), + calculateRadius(bottomRightRadius.horizontal, outlineWidth), + calculateRadius(bottomRightRadius.vertical, outlineWidth), + calculateRadius(bottomLeftRadius.horizontal, outlineWidth), + calculateRadius(bottomLeftRadius.vertical, outlineWidth), + ), + Path.Direction.CW) + + canvas.drawPath(pathForOutline, outlinePaint) + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/OutlineStyle.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/OutlineStyle.kt new file mode 100644 index 00000000000..25a974c146e --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/OutlineStyle.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 OutlineStyle { + SOLID, + DASHED, + DOTTED; + + public companion object { + @JvmStatic + public fun fromString(outlineStyle: String): OutlineStyle? { + return when (outlineStyle.lowercase()) { + "solid" -> SOLID + "dashed" -> DASHED + "dotted" -> DOTTED + else -> null + } + } + } +} 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 4047aebc6ee..32f5f94593e 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp @@ -109,6 +109,39 @@ BaseViewProps::BaseViewProps( "Style", sourceProps.borderStyles, {})), + outlineColor( + CoreFeatures::enablePropIteratorSetter ? sourceProps.outlineColor + : convertRawProp( + context, + rawProps, + "outlineColor", + sourceProps.outlineColor, + {})), + outlineOffset( + CoreFeatures::enablePropIteratorSetter + ? sourceProps.outlineOffset + : convertRawProp( + context, + rawProps, + "outlineOffset", + sourceProps.outlineOffset, + {})), + outlineStyle( + CoreFeatures::enablePropIteratorSetter ? sourceProps.outlineStyle + : convertRawProp( + context, + rawProps, + "outlineStyle", + sourceProps.outlineStyle, + {})), + outlineWidth( + CoreFeatures::enablePropIteratorSetter ? sourceProps.outlineWidth + : convertRawProp( + context, + rawProps, + "outlineWidth", + sourceProps.outlineWidth, + {})), shadowColor( CoreFeatures::enablePropIteratorSetter ? sourceProps.shadowColor : convertRawProp( @@ -346,6 +379,10 @@ void BaseViewProps::setProp( RAW_SET_PROP_SWITCH_CASE_BASIC(removeClippedSubviews); RAW_SET_PROP_SWITCH_CASE_BASIC(experimental_layoutConformance); RAW_SET_PROP_SWITCH_CASE_BASIC(cursor); + RAW_SET_PROP_SWITCH_CASE_BASIC(outlineColor); + RAW_SET_PROP_SWITCH_CASE_BASIC(outlineOffset); + RAW_SET_PROP_SWITCH_CASE_BASIC(outlineStyle); + RAW_SET_PROP_SWITCH_CASE_BASIC(outlineWidth); RAW_SET_PROP_SWITCH_CASE(filter, "filter"); RAW_SET_PROP_SWITCH_CASE(boxShadow, "boxShadow"); // events field 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 edc01556c91..0d5f605990d 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h @@ -51,6 +51,12 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps { CascadedBorderCurves borderCurves{}; // iOS only? CascadedBorderStyles borderStyles{}; + // Outline + SharedColor outlineColor{}; + Float outlineOffset{}; + OutlineStyle outlineStyle{OutlineStyle::Solid}; + Float outlineWidth{}; + // Shadow SharedColor shadowColor{}; Size shadowOffset{0, -3}; 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 ef13a094524..7e819c28e99 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp @@ -70,7 +70,8 @@ void ViewShadowNode::initialize() noexcept { isColorMeaningful(viewProps.backgroundColor) || hasBorder() || !viewProps.testId.empty() || !viewProps.boxShadow.empty() || !viewProps.backgroundImage.empty() || - HostPlatformViewTraitsInitializer::formsView(viewProps); + HostPlatformViewTraitsInitializer::formsView(viewProps) || + viewProps.outlineWidth > 0; if (formsView) { traits_.set(ShadowNodeTraits::Trait::FormsView); 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 e91ed7246d3..8f0bcbb33ea 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h @@ -755,6 +755,32 @@ inline void fromRawValue( react_native_expect(false); } +inline void fromRawValue( + const PropsParserContext& context, + const RawValue& value, + OutlineStyle& result) { + result = OutlineStyle::Solid; + react_native_expect(value.hasType()); + if (!value.hasType()) { + return; + } + auto stringValue = (std::string)value; + if (stringValue == "solid") { + result = OutlineStyle::Solid; + return; + } + if (stringValue == "dotted") { + result = OutlineStyle::Dotted; + return; + } + if (stringValue == "dashed") { + result = OutlineStyle::Dashed; + return; + } + LOG(ERROR) << "Could not parse OutlineStyle:" << stringValue; + react_native_expect(false); +} + inline void fromRawValue( const PropsParserContext& context, const RawValue& value, diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h b/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h index 42f2cbe2d21..feb396a0535 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h @@ -92,6 +92,8 @@ enum class BorderCurve : uint8_t { Circular, Continuous }; enum class BorderStyle : uint8_t { Solid, Dotted, Dashed }; +enum class OutlineStyle : uint8_t { Solid, Dotted, Dashed }; + struct CornerRadii { float vertical{0.0f}; float horizontal{0.0f}; diff --git a/packages/rn-tester/js/examples/View/ViewExample.js b/packages/rn-tester/js/examples/View/ViewExample.js index bddc7866765..e93732f2162 100644 --- a/packages/rn-tester/js/examples/View/ViewExample.js +++ b/packages/rn-tester/js/examples/View/ViewExample.js @@ -533,6 +533,122 @@ function BoxShadowExample(): React.Node { ); } +function OutlineExample(): React.Node { + const defaultStyleSize = {width: 50, height: 50}; + + return ( + + + + + + + + + + + ); +} + export default ({ title: 'View', documentationURL: 'https://reactnative.dev/docs/view', @@ -1172,5 +1288,11 @@ export default ({ name: 'box-shadow', render: BoxShadowExample, }, + { + title: 'Outline', + name: 'outline', + platform: 'android', + render: OutlineExample, + }, ], }: RNTesterModule);