Implement outline properties on Android (#46284)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/46284

This diff adds:

`outline-width`: https://developer.mozilla.org/en-US/docs/Web/CSS/outline-width
`outline-color`: https://developer.mozilla.org/en-US/docs/Web/CSS/outline-color
`outline-style`: https://developer.mozilla.org/en-US/docs/Web/CSS/outline-style
`outline-offset`: https://developer.mozilla.org/en-US/docs/Web/CSS/outline-offset

Using `BackgroundStyleApplicator`

Changelog: [Android] [Added] - Outline properties `outline-width`, `outline-color`, `outline-style` & `outline-offset`

Reviewed By: NickGerleman

Differential Revision: D61293868

fbshipit-source-id: d8787bbf1560cf46b92ad039afde9c1e7ab669da
This commit is contained in:
Jorge Cabiedes Acosta
2024-09-13 17:26:37 -07:00
committed by Facebook GitHub Bot
parent 40aaeb7181
commit 17faac4170
18 changed files with 572 additions and 5 deletions
@@ -174,6 +174,10 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
borderTopStartRadius: true,
cursor: true,
opacity: true,
outlineColor: colorAttributes,
outlineOffset: true,
outlineStyle: true,
outlineWidth: true,
pointerEvents: true,
/**
@@ -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,
@@ -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
@@ -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,
@@ -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,
@@ -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;
@@ -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<BoxShadow>): 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
}
@@ -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<T extends View, C extends LayoutShadowNode
logUnsupportedPropertyWarning(ViewProps.BORDER_TOP_RIGHT_RADIUS);
}
@ReactProp(name = ViewProps.OUTLINE_COLOR, customType = "Color")
public void setOutlineColor(T view, @Nullable Integer color) {
if (ReactNativeFeatureFlags.enableBackgroundStyleApplicator()) {
BackgroundStyleApplicator.setOutlineColor(view, color);
}
}
@ReactProp(name = ViewProps.OUTLINE_OFFSET)
public void setOutlineOffset(T view, float offset) {
if (ReactNativeFeatureFlags.enableBackgroundStyleApplicator()) {
BackgroundStyleApplicator.setOutlineOffset(view, offset);
}
}
@ReactProp(name = ViewProps.OUTLINE_STYLE)
public void setOutlineStyle(T view, @Nullable String outlineStyle) {
if (ReactNativeFeatureFlags.enableBackgroundStyleApplicator()) {
@Nullable
OutlineStyle parsedOutlineStyle =
outlineStyle == null ? null : OutlineStyle.fromString(outlineStyle);
BackgroundStyleApplicator.setOutlineStyle(view, parsedOutlineStyle);
}
}
@ReactProp(name = ViewProps.OUTLINE_WIDTH)
public void setOutlineWidth(T view, float width) {
if (ReactNativeFeatureFlags.enableBackgroundStyleApplicator()) {
BackgroundStyleApplicator.setOutlineWidth(view, width);
}
}
private void logUnsupportedPropertyWarning(String propName) {
FLog.w(ReactConstants.TAG, "%s doesn't support property '%s'", getName(), propName);
}
@@ -139,6 +139,10 @@ public object ViewProps {
public const val BOX_SHADOW: String = "boxShadow"
public const val FILTER: String = "filter"
public const val MIX_BLEND_MODE: String = "experimental_mixBlendMode"
public const val OUTLINE_COLOR: String = "outlineColor"
public const val OUTLINE_OFFSET: String = "outlineOffset"
public const val OUTLINE_STYLE: String = "outlineStyle"
public const val OUTLINE_WIDTH: String = "outlineWidth"
public const val TRANSFORM: String = "transform"
public const val TRANSFORM_ORIGIN: String = "transformOrigin"
public const val ELEVATION: String = "elevation"
@@ -40,6 +40,9 @@ internal class CompositeBackgroundDrawable(
/** Inset box-shadows */
public val innerShadows: List<Drawable> = 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<Drawable>
): 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)
}
}
@@ -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 <T> invalidatingChange(initialValue: T): ReadWriteProperty<Any?, T> =
object : ObservableProperty<T>(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)
}
}
@@ -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
}
}
}
}
@@ -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
@@ -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};
@@ -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);
@@ -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<std::string>());
if (!value.hasType<std::string>()) {
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,
@@ -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};
@@ -533,6 +533,122 @@ function BoxShadowExample(): React.Node {
);
}
function OutlineExample(): React.Node {
const defaultStyleSize = {width: 50, height: 50};
return (
<View
testID="view-test-outline"
style={{
padding: 10,
flexDirection: 'row',
gap: 20,
margin: 10,
flexWrap: 'wrap',
}}>
<View
style={[
defaultStyleSize,
{
borderColor: 'red',
borderWidth: 8,
outlineWidth: 8,
outlineColor: 'purple',
},
]}
/>
<View
style={[
defaultStyleSize,
{
borderColor: 'red',
borderWidth: 8,
outlineWidth: 8,
borderRadius: 20,
outlineColor: 'purple',
},
]}
/>
<View
style={[
defaultStyleSize,
{
borderColor: 'red',
borderWidth: 8,
outlineWidth: 8,
borderTopLeftRadius: 20,
borderBottomRightRadius: 20,
outlineColor: 'purple',
},
]}
/>
<View
style={[
defaultStyleSize,
{
borderColor: 'green',
borderWidth: 8,
outlineWidth: 8,
outlineColor: 'orange',
outlineStyle: 'solid',
outlineOffset: 5,
marginLeft: 5,
},
]}
/>
<View
style={[
defaultStyleSize,
{
borderColor: 'green',
borderWidth: 8,
outlineWidth: 8,
borderRadius: 20,
outlineColor: 'orange',
outlineStyle: 'dotted',
},
]}
/>
<View
style={[
defaultStyleSize,
{
borderColor: 'green',
borderWidth: 8,
outlineWidth: 8,
borderTopLeftRadius: 20,
borderBottomRightRadius: 20,
outlineColor: 'orange',
outlineStyle: 'dashed',
},
]}
/>
<View
style={[
defaultStyleSize,
{
width: 100,
borderColor: 'green',
borderWidth: 8,
outlineWidth: 8,
borderRadius: '100%',
outlineColor: 'orange',
},
]}
/>
<View
style={[
defaultStyleSize,
{
outlineWidth: 9,
outlineColor: 'orange',
},
]}
/>
</View>
);
}
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);