diff --git a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js index c186586d9a0983..e263366c12cd3e 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 bceab558d60ee6..71c6782b495eef 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 ae0a11716f85c0..dd79ed9ebc68af 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts @@ -320,6 +320,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 09709ab8d97761..048bab3cf84093 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 1c51017ab8a4ac..0aa6f21ba97304 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 @@ -8314,6 +8314,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 9589c1551778d2..6c998e584f8cda 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -4109,6 +4109,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/BorderStyle;)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 { @@ -4144,6 +4148,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 @@ -5616,6 +5624,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; @@ -6167,11 +6179,11 @@ public final class com/facebook/react/uimanager/style/CornerRadii { public final fun component2 ()F public final fun copy (FF)Lcom/facebook/react/uimanager/style/CornerRadii; public static synthetic fun copy$default (Lcom/facebook/react/uimanager/style/CornerRadii;FFILjava/lang/Object;)Lcom/facebook/react/uimanager/style/CornerRadii; + public final fun dpToPx ()Lcom/facebook/react/uimanager/style/CornerRadii; public fun equals (Ljava/lang/Object;)Z public final fun getHorizontal ()F public final fun getVertical ()F public fun hashCode ()I - public final fun toPixelFromDIP ()Lcom/facebook/react/uimanager/style/CornerRadii; public fun toString ()Ljava/lang/String; } 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 d6368fd67ec303..540354fd669c31 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 @@ -24,7 +24,9 @@ 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.MIN_INSET_BOX_SHADOW_SDK_VERSION +import com.facebook.react.uimanager.drawable.MIN_OUTLINE_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 @@ -80,6 +82,64 @@ public object BackgroundStyleApplicator { } } + @JvmStatic + public fun setOutlineColor(view: View, @ColorInt outlineColor: Int?): Unit { + val outline = getOutlineDrawable(view) + if (outline != null && + outlineColor != null && + Build.VERSION.SDK_INT >= MIN_OUTLINE_SDK_VERSION) { + outline.outlineColor = outlineColor + } + } + + @JvmStatic + public fun setOutlineOffset(view: View, outlineOffset: Float): Unit { + val outline = getOutlineDrawable(view) + if (outline != null && Build.VERSION.SDK_INT >= MIN_OUTLINE_SDK_VERSION) { + outline.outlineOffset = outlineOffset + } + } + + @JvmStatic + public fun setOutlineStyle(view: View, outlineStyle: BorderStyle?): Unit { + val outline = getOutlineDrawable(view) + if (outline != null && + outlineStyle != null && + Build.VERSION.SDK_INT >= MIN_OUTLINE_SDK_VERSION) { + outline.outlineStyle = outlineStyle + } + } + + @JvmStatic + public fun setOutlineWidth(view: View, width: Float): Unit { + val outline = getOutlineDrawable(view) + if (outline != null && Build.VERSION.SDK_INT >= MIN_OUTLINE_SDK_VERSION) { + outline.outlineWidth = width.dpToPx() + } + } + + private fun getOutlineDrawable(view: View): OutlineDrawable? { + if (Build.VERSION.SDK_INT < MIN_OUTLINE_SDK_VERSION) { + return null + } + + var outline = ensureCompositeBackgroundDrawable(view).outline + if (outline == null) { + outline = + OutlineDrawable( + context = view.context, + borderRadius = ensureCSSBackground(view).borderRadius, + outlineColor = Color.BLACK, + outlineOffset = 0f, + outlineStyle = BorderStyle.SOLID, + outlineWidth = 0f, + ) + view.background = ensureCompositeBackgroundDrawable(view).withNewOutline(outline) + } + + return outline + } + @JvmStatic public fun getBorderWidth(view: View, edge: LogicalEdge): Float? { val width = getCSSBackground(view)?.getBorderWidth(edge.toSpacingType()) 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 382055fbc79c50..8475289a27bed9 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.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.BorderStyle; import com.facebook.react.uimanager.util.ReactFindViewUtil; import java.util.ArrayList; import java.util.HashMap; @@ -772,6 +774,37 @@ public void setBorderTopRightRadius(T view, float borderRadius) { 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, PixelUtil.toPixelFromDIP(offset)); + } + } + + @ReactProp(name = ViewProps.OUTLINE_STYLE) + public void setOutlineStyle(T view, @Nullable String outlineStyle) { + if (ReactNativeFeatureFlags.enableBackgroundStyleApplicator()) { + @Nullable + BorderStyle parsedOutlineStyle = + outlineStyle == null ? null : BorderStyle.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); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt index dfcd023945bd73..6ba7e52b864529 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt @@ -139,6 +139,10 @@ public object ViewProps { public const val BOX_SHADOW: String = "experimental_boxShadow" public const val FILTER: String = "experimental_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" 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 205bf491f798af..93a2385a44e657 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 @@ -673,10 +673,10 @@ private void updatePath() { mContext, PixelUtil.toDIPFromPixel(mOuterClipTempRectForBorderRadius.width()), PixelUtil.toDIPFromPixel(mOuterClipTempRectForBorderRadius.height())); - CornerRadii topLeftRadius = mComputedBorderRadius.getTopLeft().toPixelFromDIP(); - CornerRadii topRightRadius = mComputedBorderRadius.getTopRight().toPixelFromDIP(); - CornerRadii bottomLeftRadius = mComputedBorderRadius.getBottomLeft().toPixelFromDIP(); - CornerRadii bottomRightRadius = mComputedBorderRadius.getBottomRight().toPixelFromDIP(); + CornerRadii topLeftRadius = mComputedBorderRadius.getTopLeft().dpToPx(); + CornerRadii topRightRadius = mComputedBorderRadius.getTopRight().dpToPx(); + CornerRadii bottomLeftRadius = mComputedBorderRadius.getBottomLeft().dpToPx(); + CornerRadii bottomRightRadius = mComputedBorderRadius.getBottomRight().dpToPx(); final float innerTopLeftRadiusX = getInnerBorderRadius(topLeftRadius.getHorizontal(), borderWidth.left); 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 index 1da46a7d6b221e..aae0e875171e97 100644 --- 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 @@ -23,6 +23,7 @@ internal class CompositeBackgroundDrawable( * TextInput */ public val originalBackground: Drawable? = null, + public val outline: OutlineDrawable? = null, /** Non-inset box shadows */ public val outerShadows: List = emptyList(), @@ -47,6 +48,7 @@ internal class CompositeBackgroundDrawable( // z-ordering of user-provided shadow-list is opposite direction of LayerDrawable // z-ordering // https://drafts.csswg.org/css-backgrounds/#shadow-layers + outline, *outerShadows.asReversed().toTypedArray(), cssBackground, feedbackUnderlay, @@ -67,7 +69,7 @@ internal class CompositeBackgroundDrawable( cssBackground: CSSBackgroundDrawable? ): CompositeBackgroundDrawable { return CompositeBackgroundDrawable( - originalBackground, outerShadows, cssBackground, feedbackUnderlay, innerShadows) + originalBackground, outline, outerShadows, cssBackground, feedbackUnderlay, innerShadows) } public fun withNewShadows( @@ -75,11 +77,16 @@ internal class CompositeBackgroundDrawable( innerShadows: List ): CompositeBackgroundDrawable { return CompositeBackgroundDrawable( - originalBackground, outerShadows, cssBackground, feedbackUnderlay, innerShadows) + originalBackground, outline, outerShadows, cssBackground, feedbackUnderlay, innerShadows) + } + + public fun withNewOutline(outline: OutlineDrawable): CompositeBackgroundDrawable { + return CompositeBackgroundDrawable( + originalBackground, outline, outerShadows, cssBackground, feedbackUnderlay, innerShadows) } public fun withNewFeedbackUnderlay(newUnderlay: Drawable?): CompositeBackgroundDrawable { return CompositeBackgroundDrawable( - originalBackground, outerShadows, cssBackground, newUnderlay, innerShadows) + originalBackground, outline, outerShadows, cssBackground, newUnderlay, innerShadows) } } 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 00000000000000..d884e3dd67f21f --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/OutlineDrawable.kt @@ -0,0 +1,179 @@ +/* + * 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 androidx.annotation.RequiresApi +import com.facebook.react.uimanager.PixelUtil.dpToPx +import com.facebook.react.uimanager.style.BorderRadiusStyle +import com.facebook.react.uimanager.style.BorderStyle +import com.facebook.react.uimanager.style.ComputedBorderRadius +import com.facebook.react.uimanager.style.CornerRadii +import kotlin.math.roundToInt +import kotlin.properties.ObservableProperty +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +internal const val MIN_OUTLINE_SDK_VERSION = 29 + +/** Draws outline https://drafts.csswg.org/css-ui/#outline */ +@RequiresApi(MIN_OUTLINE_SDK_VERSION) +internal class OutlineDrawable( + private val context: Context, + borderRadius: BorderRadiusStyle? = null, + outlineColor: Int, + outlineOffset: Float, + outlineStyle: BorderStyle, + 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: BorderStyle = 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: BorderStyle, outlineWidth: Float): PathEffect? { + return when (style) { + BorderStyle.SOLID -> null + BorderStyle.DASHED -> + DashPathEffect( + floatArrayOf(outlineWidth * 3, outlineWidth * 3, outlineWidth * 3, outlineWidth * 3), 0f) + BorderStyle.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?.dpToPx() ?: CornerRadii(0f, 0f) + val topRightRadius = computedBorderRadius?.topRight?.dpToPx() ?: CornerRadii(0f, 0f) + val bottomLeftRadius = computedBorderRadius?.bottomLeft?.dpToPx() ?: CornerRadii(0f, 0f) + val bottomRightRadius = + computedBorderRadius?.bottomRight?.dpToPx() ?: 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/CornerRadii.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/CornerRadii.kt index 9c882979672359..4f4eeb38230bf9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/CornerRadii.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/CornerRadii.kt @@ -13,7 +13,7 @@ public data class CornerRadii( val horizontal: Float = 0f, val vertical: Float = 0f, ) { - public fun toPixelFromDIP(): CornerRadii { + public fun dpToPx(): CornerRadii { return CornerRadii(PixelUtil.toPixelFromDIP(horizontal), PixelUtil.toPixelFromDIP(vertical)) } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java index 5ec379de2d9004..aa64edccf33386 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java @@ -1082,10 +1082,10 @@ private void dispatchOverflowDraw(Canvas canvas) { mPath = new Path(); } - CornerRadii topLeftRadius = borderRadius.getTopLeft().toPixelFromDIP(); - CornerRadii topRightRadius = borderRadius.getTopRight().toPixelFromDIP(); - CornerRadii bottomLeftRadius = borderRadius.getBottomLeft().toPixelFromDIP(); - CornerRadii bottomRightRadius = borderRadius.getBottomRight().toPixelFromDIP(); + CornerRadii topLeftRadius = borderRadius.getTopLeft().dpToPx(); + CornerRadii topRightRadius = borderRadius.getTopRight().dpToPx(); + CornerRadii bottomLeftRadius = borderRadius.getBottomLeft().dpToPx(); + CornerRadii bottomRightRadius = borderRadius.getBottomRight().dpToPx(); mPath.rewind(); mPath.addRoundRect( diff --git a/packages/rn-tester/js/examples/View/ViewExample.js b/packages/rn-tester/js/examples/View/ViewExample.js index de44881b2acde0..e821dcbbdab15a 100644 --- a/packages/rn-tester/js/examples/View/ViewExample.js +++ b/packages/rn-tester/js/examples/View/ViewExample.js @@ -533,6 +533,112 @@ 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 +1278,11 @@ export default ({ name: 'box-shadow', render: BoxShadowExample, }, + { + title: 'Outline', + name: 'outline', + platform: 'android', + render: OutlineExample, + }, ], }: RNTesterModule);