mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
40aaeb7181
commit
17faac4170
@@ -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;
|
||||
|
||||
+67
@@ -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
|
||||
}
|
||||
|
||||
+33
@@ -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);
|
||||
}
|
||||
|
||||
+4
@@ -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"
|
||||
|
||||
+13
-4
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+182
@@ -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)
|
||||
}
|
||||
}
|
||||
+26
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user