From 181ed33ab0ae3a67c5bcb73de315fb6d5e697388 Mon Sep 17 00:00:00 2001 From: Jorge Cabiedes Acosta Date: Thu, 9 May 2024 11:18:27 -0700 Subject: [PATCH] Add percentages for border-radius props (#44408) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/44408 Why? Previously we didn't support using percentages like: ``` style={{ width=100, height=100, borderRadius='100%', }} ``` These percentages refer to the corresponding dimension of the border box. What? - Added LengthPercentage class and LengthPercentageType enum. To track when we are dealing with percentage vs points - Now radius properties start as Dynamic which then get transformed into LengthPercentage. - Modified certain function parameters so we can consider height and width when resolving BorderRadius values With this we conditionally calculate the corresponding point (dp) value for a given percentage (considering size). Ex: ``` result = {raw_percentage_value} / 100 * (max(height, width)) ``` We know the maximum border radius for our current implementation is half the dp of the shorter side of our view, hence why we consider half our maximum view side as equivalent to 100%. Note: We still don't support vertical/horizontal border radii ## Changelog: [Android][Added] - Added support for using percentages when defining border radius related properties. Reviewed By: NickGerleman Differential Revision: D56943825 fbshipit-source-id: 3e5a9933ca90e499aff9c7d2561f5f6bb55157da --- .../ReactAndroid/api/ReactAndroid.api | 118 +++++++++++------- .../react/uimanager/LengthPercentage.kt | 73 +++++++++++ .../drawable/CSSBackgroundDrawable.java | 48 +++++-- .../uimanager/style/BorderRadiusStyle.kt | 95 +++++++------- .../uimanager/style/ComputedBorderRadius.kt | 2 + .../react/views/view/ReactViewGroup.java | 39 +++--- .../react/views/view/ReactViewManager.java | 30 ++--- 7 files changed, 270 insertions(+), 135 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/LengthPercentage.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 2d644928821..1043824e561 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -4188,6 +4188,25 @@ public class com/facebook/react/uimanager/LayoutShadowNode$$PropsSetter : com/fa public synthetic fun setProperty (Lcom/facebook/react/uimanager/ReactShadowNode;Ljava/lang/String;Ljava/lang/Object;)V } +public final class com/facebook/react/uimanager/LengthPercentage { + public static final field Companion Lcom/facebook/react/uimanager/LengthPercentage$Companion; + public fun ()V + public fun (FLcom/facebook/react/uimanager/LengthPercentageType;)V + public final fun resolve (FF)F + public static final fun setFromDynamic (Lcom/facebook/react/bridge/Dynamic;)Lcom/facebook/react/uimanager/LengthPercentage; +} + +public final class com/facebook/react/uimanager/LengthPercentage$Companion { + public final fun setFromDynamic (Lcom/facebook/react/bridge/Dynamic;)Lcom/facebook/react/uimanager/LengthPercentage; +} + +public final class com/facebook/react/uimanager/LengthPercentageType : java/lang/Enum { + public static final field PERCENT Lcom/facebook/react/uimanager/LengthPercentageType; + public static final field POINT Lcom/facebook/react/uimanager/LengthPercentageType; + public static fun valueOf (Ljava/lang/String;)Lcom/facebook/react/uimanager/LengthPercentageType; + public static fun values ()[Lcom/facebook/react/uimanager/LengthPercentageType; +} + public class com/facebook/react/uimanager/MatrixMathHelper { public fun ()V public static fun applyPerspective ([DD)V @@ -5483,6 +5502,7 @@ public class com/facebook/react/uimanager/drawable/CSSBackgroundDrawable : andro public fun getBorderColor (I)I public fun getBorderRadius ()Lcom/facebook/react/uimanager/style/BorderRadiusStyle; public fun getBorderWidthOrDefaultTo (FI)F + public fun getComputedBorderRadius ()Lcom/facebook/react/uimanager/style/ComputedBorderRadius; public fun getDirectionAwareBorderInsets ()Landroid/graphics/RectF; public fun getFullBorderWidth ()F public fun getOpacity ()I @@ -5494,7 +5514,7 @@ public class com/facebook/react/uimanager/drawable/CSSBackgroundDrawable : andro public fun paddingBoxPath ()Landroid/graphics/Path; public fun setAlpha (I)V public fun setBorderColor (IFF)V - public fun setBorderRadius (Lcom/facebook/react/uimanager/style/BorderRadiusProp;Ljava/lang/Float;)V + public fun setBorderRadius (Lcom/facebook/react/uimanager/style/BorderRadiusProp;Lcom/facebook/react/uimanager/LengthPercentage;)V public fun setBorderRadius (Lcom/facebook/react/uimanager/style/BorderRadiusStyle;)V public fun setBorderStyle (Ljava/lang/String;)V public fun setBorderWidth (IF)V @@ -5819,60 +5839,61 @@ public final class com/facebook/react/uimanager/style/BorderRadiusProp : java/la public final class com/facebook/react/uimanager/style/BorderRadiusStyle { public fun ()V - public fun (Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;)V - public synthetic fun (Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;)V + public synthetic fun (Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (Ljava/util/List;)V - public final fun component1 ()Ljava/lang/Float; - public final fun component10 ()Ljava/lang/Float; - public final fun component11 ()Ljava/lang/Float; - public final fun component12 ()Ljava/lang/Float; - public final fun component13 ()Ljava/lang/Float; - public final fun component2 ()Ljava/lang/Float; - public final fun component3 ()Ljava/lang/Float; - public final fun component4 ()Ljava/lang/Float; - public final fun component5 ()Ljava/lang/Float; - public final fun component6 ()Ljava/lang/Float; - public final fun component7 ()Ljava/lang/Float; - public final fun component8 ()Ljava/lang/Float; - public final fun component9 ()Ljava/lang/Float; - public final fun copy (Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;)Lcom/facebook/react/uimanager/style/BorderRadiusStyle; - public static synthetic fun copy$default (Lcom/facebook/react/uimanager/style/BorderRadiusStyle;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Float;ILjava/lang/Object;)Lcom/facebook/react/uimanager/style/BorderRadiusStyle; + public final fun component1 ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun component10 ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun component11 ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun component12 ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun component13 ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun component2 ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun component3 ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun component4 ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun component5 ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun component6 ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun component7 ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun component8 ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun component9 ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun copy (Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;)Lcom/facebook/react/uimanager/style/BorderRadiusStyle; + public static synthetic fun copy$default (Lcom/facebook/react/uimanager/style/BorderRadiusStyle;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;Lcom/facebook/react/uimanager/LengthPercentage;ILjava/lang/Object;)Lcom/facebook/react/uimanager/style/BorderRadiusStyle; public fun equals (Ljava/lang/Object;)Z - public final fun get (Lcom/facebook/react/uimanager/style/BorderRadiusProp;)Ljava/lang/Float; - public final fun getBottomEnd ()Ljava/lang/Float; - public final fun getBottomLeft ()Ljava/lang/Float; - public final fun getBottomRight ()Ljava/lang/Float; - public final fun getBottomStart ()Ljava/lang/Float; - public final fun getEndEnd ()Ljava/lang/Float; - public final fun getEndStart ()Ljava/lang/Float; - public final fun getStartEnd ()Ljava/lang/Float; - public final fun getStartStart ()Ljava/lang/Float; - public final fun getTopEnd ()Ljava/lang/Float; - public final fun getTopLeft ()Ljava/lang/Float; - public final fun getTopRight ()Ljava/lang/Float; - public final fun getTopStart ()Ljava/lang/Float; - public final fun getUniform ()Ljava/lang/Float; + public final fun get (Lcom/facebook/react/uimanager/style/BorderRadiusProp;)Lcom/facebook/react/uimanager/LengthPercentage; + public final fun getBottomEnd ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun getBottomLeft ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun getBottomRight ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun getBottomStart ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun getEndEnd ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun getEndStart ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun getStartEnd ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun getStartStart ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun getTopEnd ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun getTopLeft ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun getTopRight ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun getTopStart ()Lcom/facebook/react/uimanager/LengthPercentage; + public final fun getUniform ()Lcom/facebook/react/uimanager/LengthPercentage; public final fun hasRoundedBorders ()Z public fun hashCode ()I - public final fun resolve (ILandroid/content/Context;)Lcom/facebook/react/uimanager/style/ComputedBorderRadius; - public final fun set (Lcom/facebook/react/uimanager/style/BorderRadiusProp;Ljava/lang/Float;)V - public final fun setBottomEnd (Ljava/lang/Float;)V - public final fun setBottomLeft (Ljava/lang/Float;)V - public final fun setBottomRight (Ljava/lang/Float;)V - public final fun setBottomStart (Ljava/lang/Float;)V - public final fun setEndEnd (Ljava/lang/Float;)V - public final fun setEndStart (Ljava/lang/Float;)V - public final fun setStartEnd (Ljava/lang/Float;)V - public final fun setStartStart (Ljava/lang/Float;)V - public final fun setTopEnd (Ljava/lang/Float;)V - public final fun setTopLeft (Ljava/lang/Float;)V - public final fun setTopRight (Ljava/lang/Float;)V - public final fun setTopStart (Ljava/lang/Float;)V - public final fun setUniform (Ljava/lang/Float;)V + public final fun resolve (ILandroid/content/Context;FF)Lcom/facebook/react/uimanager/style/ComputedBorderRadius; + public final fun set (Lcom/facebook/react/uimanager/style/BorderRadiusProp;Lcom/facebook/react/uimanager/LengthPercentage;)V + public final fun setBottomEnd (Lcom/facebook/react/uimanager/LengthPercentage;)V + public final fun setBottomLeft (Lcom/facebook/react/uimanager/LengthPercentage;)V + public final fun setBottomRight (Lcom/facebook/react/uimanager/LengthPercentage;)V + public final fun setBottomStart (Lcom/facebook/react/uimanager/LengthPercentage;)V + public final fun setEndEnd (Lcom/facebook/react/uimanager/LengthPercentage;)V + public final fun setEndStart (Lcom/facebook/react/uimanager/LengthPercentage;)V + public final fun setStartEnd (Lcom/facebook/react/uimanager/LengthPercentage;)V + public final fun setStartStart (Lcom/facebook/react/uimanager/LengthPercentage;)V + public final fun setTopEnd (Lcom/facebook/react/uimanager/LengthPercentage;)V + public final fun setTopLeft (Lcom/facebook/react/uimanager/LengthPercentage;)V + public final fun setTopRight (Lcom/facebook/react/uimanager/LengthPercentage;)V + public final fun setTopStart (Lcom/facebook/react/uimanager/LengthPercentage;)V + public final fun setUniform (Lcom/facebook/react/uimanager/LengthPercentage;)V public fun toString ()Ljava/lang/String; } public final class com/facebook/react/uimanager/style/ComputedBorderRadius { + public fun ()V public fun (FFFF)V public final fun component1 ()F public final fun component2 ()F @@ -7904,7 +7925,7 @@ public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGro public fun setBorderColor (IFF)V public fun setBorderRadius (F)V public fun setBorderRadius (FI)V - public fun setBorderRadius (Lcom/facebook/react/uimanager/style/BorderRadiusProp;Ljava/lang/Float;)V + public fun setBorderRadius (Lcom/facebook/react/uimanager/style/BorderRadiusProp;Lcom/facebook/react/uimanager/LengthPercentage;)V public fun setBorderStyle (Ljava/lang/String;)V public fun setBorderWidth (IF)V public fun setHitSlopRect (Landroid/graphics/Rect;)V @@ -7940,6 +7961,7 @@ public class com/facebook/react/views/view/ReactViewManager : com/facebook/react public fun setBackfaceVisibility (Lcom/facebook/react/views/view/ReactViewGroup;Ljava/lang/String;)V public fun setBorderColor (Lcom/facebook/react/views/view/ReactViewGroup;ILjava/lang/Integer;)V public fun setBorderRadius (Lcom/facebook/react/views/view/ReactViewGroup;IF)V + public fun setBorderRadius (Lcom/facebook/react/views/view/ReactViewGroup;ILcom/facebook/react/bridge/Dynamic;)V public fun setBorderStyle (Lcom/facebook/react/views/view/ReactViewGroup;Ljava/lang/String;)V public fun setBorderWidth (Lcom/facebook/react/views/view/ReactViewGroup;IF)V public fun setCollapsable (Lcom/facebook/react/views/view/ReactViewGroup;Z)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/LengthPercentage.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/LengthPercentage.kt new file mode 100644 index 00000000000..2616b65c50f --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/LengthPercentage.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.uimanager + +import com.facebook.common.logging.FLog +import com.facebook.react.bridge.Dynamic +import com.facebook.react.bridge.ReadableType +import com.facebook.react.common.ReactConstants +import java.lang.NumberFormatException + +public enum class LengthPercentageType { + POINT, + PERCENT, +} + +public class LengthPercentage( + private val value: Float, + private val unit: LengthPercentageType, +) { + public companion object { + @JvmStatic + public fun setFromDynamic(dynamic: Dynamic): LengthPercentage? { + return when (dynamic.getType()) { + ReadableType.Number -> { + val value = dynamic.asDouble() + if (value > 0f) { + LengthPercentage(PixelUtil.toPixelFromDIP(value), LengthPercentageType.POINT) + } else { + null + } + } + ReadableType.String -> { + val s = dynamic.asString() + if (s.endsWith("%")) { + try { + val value = s.substring(0, s.length - 1).toFloat() + if (value > 0f) { + LengthPercentage(value, LengthPercentageType.PERCENT) + } else { + null + } + } catch (e: NumberFormatException) { + FLog.w(ReactConstants.TAG, "Invalid percentage format: $s") + null + } + } else { + FLog.w(ReactConstants.TAG, "Invalid string value: $s") + null + } + } + else -> { + FLog.w(ReactConstants.TAG, "Unsupported type for radius property: ${dynamic.getType()}") + null + } + } + } + } + + public fun resolve(width: Float, height: Float): Float { + if (unit == LengthPercentageType.PERCENT) { + return (value / 100) * Math.max(width, height) + } + + return value + } + + public constructor() : this(0f, LengthPercentageType.POINT) +} 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 21094fc73d5..4e795bf6f23 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 @@ -27,11 +27,14 @@ import androidx.core.graphics.ColorUtils; import com.facebook.react.common.annotations.VisibleForTesting; import com.facebook.react.modules.i18nmanager.I18nUtil; import com.facebook.react.uimanager.FloatUtil; +import com.facebook.react.uimanager.LengthPercentage; +import com.facebook.react.uimanager.LengthPercentageType; import com.facebook.react.uimanager.Spacing; import com.facebook.react.uimanager.style.BorderRadiusProp; import com.facebook.react.uimanager.style.BorderRadiusStyle; import com.facebook.react.uimanager.style.ComputedBorderRadius; import java.util.Locale; +import java.util.Objects; /** * A subclass of {@link Drawable} used for background of {@link @@ -114,6 +117,7 @@ public class CSSBackgroundDrawable extends Drawable { private final float mGapBetweenPaths = 0.8f; private BorderRadiusStyle mBorderRadius = new BorderRadiusStyle(); + private ComputedBorderRadius mComputedBorderRadius = new ComputedBorderRadius(); private final Context mContext; private int mLayoutDirection; @@ -235,23 +239,36 @@ public class CSSBackgroundDrawable extends Drawable { } /** - * @deprecated Use {@link #setBorderRadius(BorderRadiusProp, Float)} instead. + * @deprecated Use {@link #setBorderRadius(BorderRadiusProp, LengthPercentage)} instead. */ public void setRadius(float radius) { @Nullable Float boxedRadius = Float.isNaN(radius) ? null : Float.valueOf(radius); - setBorderRadius(BorderRadiusProp.BORDER_RADIUS, boxedRadius); + if (boxedRadius == null) { + setBorderRadius(BorderRadiusProp.BORDER_RADIUS, null); + } else { + setBorderRadius( + BorderRadiusProp.BORDER_RADIUS, + new LengthPercentage(boxedRadius, LengthPercentageType.POINT)); + } } /** - * @deprecated Use {@link #setBorderRadius(BorderRadiusProp, Float)} instead. + * @deprecated Use {@link #setBorderRadius(BorderRadiusProp, LengthPercentage)} instead. */ public void setRadius(float radius, int position) { @Nullable Float boxedRadius = Float.isNaN(radius) ? null : Float.valueOf(radius); - setBorderRadius(BorderRadiusProp.values()[position], boxedRadius); + + if (boxedRadius == null) { + mBorderRadius.set(BorderRadiusProp.values()[position], null); + } else { + setBorderRadius( + BorderRadiusProp.values()[position], + new LengthPercentage(boxedRadius, LengthPercentageType.POINT)); + } } - public void setBorderRadius(BorderRadiusProp property, @Nullable Float radius) { - if (!FloatUtil.floatsEqual(mBorderRadius.getUniform(), radius)) { + public void setBorderRadius(BorderRadiusProp property, @Nullable LengthPercentage radius) { + if (!Objects.equals(radius, mBorderRadius.get(property))) { mBorderRadius.set(property, radius); mNeedUpdatePathForBorderRadius = true; invalidateSelf(); @@ -266,6 +283,10 @@ public class CSSBackgroundDrawable extends Drawable { return mBorderRadius; } + public ComputedBorderRadius getComputedBorderRadius() { + return mComputedBorderRadius; + } + public void setColor(int color) { mColor = color; invalidateSelf(); @@ -568,11 +589,16 @@ public class CSSBackgroundDrawable extends Drawable { mTempRectForCenterDrawPath.left += borderWidth.left * 0.5f; mTempRectForCenterDrawPath.right -= borderWidth.right * 0.5f; - ComputedBorderRadius radius = mBorderRadius.resolve(mLayoutDirection, mContext); - float topLeftRadius = radius.getTopLeft(); - float topRightRadius = radius.getTopRight(); - float bottomLeftRadius = radius.getBottomLeft(); - float bottomRightRadius = radius.getBottomRight(); + mComputedBorderRadius = + mBorderRadius.resolve( + mLayoutDirection, + mContext, + mOuterClipTempRectForBorderRadius.width(), + mOuterClipTempRectForBorderRadius.height()); + float topLeftRadius = mComputedBorderRadius.getTopLeft(); + float topRightRadius = mComputedBorderRadius.getTopRight(); + float bottomLeftRadius = mComputedBorderRadius.getBottomLeft(); + float bottomRightRadius = mComputedBorderRadius.getBottomRight(); final float innerTopLeftRadiusX = Math.max(topLeftRadius - borderWidth.left, 0); final float innerTopLeftRadiusY = Math.max(topLeftRadius - borderWidth.top, 0); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BorderRadiusStyle.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BorderRadiusStyle.kt index 0683b01b073..0f475b4a327 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BorderRadiusStyle.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BorderRadiusStyle.kt @@ -10,9 +10,11 @@ package com.facebook.react.uimanager.style import android.content.Context import android.util.LayoutDirection import com.facebook.react.modules.i18nmanager.I18nUtil +import com.facebook.react.uimanager.LengthPercentage /** Represents the collection of possible border radius style properties. */ public enum class BorderRadiusProp { + BORDER_RADIUS, BORDER_TOP_LEFT_RADIUS, BORDER_TOP_RIGHT_RADIUS, BORDER_BOTTOM_RIGHT_RADIUS, @@ -25,30 +27,29 @@ public enum class BorderRadiusProp { BORDER_END_START_RADIUS, BORDER_START_END_RADIUS, BORDER_START_START_RADIUS, - BORDER_RADIUS, } /** Represents all logical properties and shorthands for border radius. */ public data class BorderRadiusStyle( - var uniform: Float? = null, - var topLeft: Float? = null, - var topRight: Float? = null, - var bottomLeft: Float? = null, - var bottomRight: Float? = null, - var topStart: Float? = null, - var topEnd: Float? = null, - var bottomStart: Float? = null, - var bottomEnd: Float? = null, - var startStart: Float? = null, - var startEnd: Float? = null, - var endStart: Float? = null, - var endEnd: Float? = null + var uniform: LengthPercentage? = null, + var topLeft: LengthPercentage? = null, + var topRight: LengthPercentage? = null, + var bottomLeft: LengthPercentage? = null, + var bottomRight: LengthPercentage? = null, + var topStart: LengthPercentage? = null, + var topEnd: LengthPercentage? = null, + var bottomStart: LengthPercentage? = null, + var bottomEnd: LengthPercentage? = null, + var startStart: LengthPercentage? = null, + var startEnd: LengthPercentage? = null, + var endStart: LengthPercentage? = null, + var endEnd: LengthPercentage? = null ) { - public constructor(properties: List>) : this() { + public constructor(properties: List>) : this() { properties.forEach { (k, v) -> set(k, v) } } - public fun set(property: BorderRadiusProp, value: Float?) { + public fun set(property: BorderRadiusProp, value: LengthPercentage?) { when (property) { BorderRadiusProp.BORDER_RADIUS -> uniform = value BorderRadiusProp.BORDER_TOP_LEFT_RADIUS -> topLeft = value @@ -66,7 +67,7 @@ public data class BorderRadiusStyle( } } - public fun get(property: BorderRadiusProp): Float? { + public fun get(property: BorderRadiusProp): LengthPercentage? { return when (property) { BorderRadiusProp.BORDER_RADIUS -> uniform BorderRadiusProp.BORDER_TOP_LEFT_RADIUS -> topLeft @@ -85,45 +86,55 @@ public data class BorderRadiusStyle( } public fun hasRoundedBorders(): Boolean { - return ((uniform ?: 0f) > 0f) || - ((topLeft ?: 0f) > 0f) || - ((topRight ?: 0f) > 0f) || - ((bottomLeft ?: 0f) > 0f) || - ((bottomRight ?: 0f) > 0f) || - ((topStart ?: 0f) > 0f) || - ((topEnd ?: 0f) > 0f) || - ((bottomStart ?: 0f) > 0f) || - ((bottomEnd ?: 0f) > 0f) || - ((startStart ?: 0f) > 0f) || - ((startEnd ?: 0f) > 0f) || - ((endStart ?: 0f) > 0f) || - ((endEnd ?: 0f) > 0f) + return uniform != null || + topLeft != null || + topRight != null || + bottomLeft != null || + bottomRight != null || + topStart != null || + topEnd != null || + bottomStart != null || + bottomEnd != null || + startStart != null || + startEnd != null || + endStart != null || + endEnd != null } public fun resolve( layoutDirection: Int, context: Context, + width: Float, + height: Float, ): ComputedBorderRadius { + val topLeft: LengthPercentage? = startStart ?: topStart ?: topLeft ?: uniform + val topRight: LengthPercentage? = endStart ?: topEnd ?: topRight ?: uniform + val bottomLeft: LengthPercentage? = startEnd ?: bottomStart ?: bottomLeft ?: uniform + val bottomRight: LengthPercentage? = endEnd ?: bottomEnd ?: bottomRight ?: uniform + return when (layoutDirection) { LayoutDirection.LTR -> ComputedBorderRadius( - topLeft = startStart ?: topStart ?: topLeft ?: uniform ?: 0f, - topRight = endStart ?: topEnd ?: topRight ?: uniform ?: 0f, - bottomLeft = startEnd ?: bottomStart ?: bottomLeft ?: uniform ?: 0f, - bottomRight = endEnd ?: bottomEnd ?: bottomRight ?: uniform ?: 0f) + topLeft = topLeft?.resolve(width, height) ?: 0f, + topRight = topRight?.resolve(width, height) ?: 0f, + bottomLeft = bottomLeft?.resolve(width, height) ?: 0f, + bottomRight = bottomRight?.resolve(width, height) ?: 0f, + ) LayoutDirection.RTL -> if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(context)) { ComputedBorderRadius( - topLeft = endStart ?: topEnd ?: topRight ?: uniform ?: 0f, - topRight = startStart ?: topStart ?: topLeft ?: uniform ?: 0f, - bottomLeft = endEnd ?: bottomStart ?: bottomRight ?: uniform ?: 0f, - bottomRight = startEnd ?: bottomEnd ?: bottomLeft ?: uniform ?: 0f) + topLeft = topRight?.resolve(width, height) ?: 0f, + topRight = topLeft?.resolve(width, height) ?: 0f, + bottomLeft = bottomRight?.resolve(width, height) ?: 0f, + bottomRight = bottomLeft?.resolve(width, height) ?: 0f, + ) } else { ComputedBorderRadius( - topLeft = endStart ?: topEnd ?: topLeft ?: uniform ?: 0f, - topRight = startStart ?: topStart ?: topRight ?: uniform ?: 0f, - bottomLeft = endEnd ?: bottomStart ?: bottomLeft ?: uniform ?: 0f, - bottomRight = startEnd ?: bottomEnd ?: bottomRight ?: uniform ?: 0f) + topLeft = topRight?.resolve(width, height) ?: 0f, + topRight = topLeft?.resolve(width, height) ?: 0f, + bottomLeft = bottomRight?.resolve(width, height) ?: 0f, + bottomRight = bottomLeft?.resolve(width, height) ?: 0f, + ) } else -> throw IllegalArgumentException("Expected resolved layout direction") } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ComputedBorderRadius.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ComputedBorderRadius.kt index 181bc4ecba0..d337e80ba35 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ComputedBorderRadius.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ComputedBorderRadius.kt @@ -17,4 +17,6 @@ public data class ComputedBorderRadius( public fun hasRoundedBorders(): Boolean { return topLeft > 0f || topRight > 0f || bottomLeft > 0f || bottomRight > 0f } + + public constructor() : this(0f, 0f, 0f, 0f) } 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 ee31c48404b..ce066636ca8 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 @@ -37,6 +37,7 @@ import com.facebook.react.touch.OnInterceptTouchEventListener; import com.facebook.react.touch.ReactHitSlopView; import com.facebook.react.touch.ReactInterceptingViewGroup; import com.facebook.react.uimanager.IllegalViewOperationException; +import com.facebook.react.uimanager.LengthPercentage; import com.facebook.react.uimanager.MeasureSpecAssertions; import com.facebook.react.uimanager.PointerEvents; import com.facebook.react.uimanager.ReactClippingProhibitedView; @@ -123,7 +124,7 @@ public class ReactViewGroup extends ViewGroup private @Nullable String mOverflow; private PointerEvents mPointerEvents; private @Nullable ChildrenLayoutChangeListener mChildrenLayoutChangeListener; - private @Nullable ReactViewBackgroundDrawable mReactBackgroundDrawable; + private @Nullable CSSBackgroundDrawable mCSSBackgroundDrawable; private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener; private boolean mNeedsOffscreenAlphaCompositing; private @Nullable ViewGroupDrawingOrderHelper mDrawingOrderHelper; @@ -153,7 +154,7 @@ public class ReactViewGroup extends ViewGroup mOverflow = null; mPointerEvents = PointerEvents.AUTO; mChildrenLayoutChangeListener = null; - mReactBackgroundDrawable = null; + mCSSBackgroundDrawable = null; mOnInterceptTouchEventListener = null; mNeedsOffscreenAlphaCompositing = false; mDrawingOrderHelper = null; @@ -200,8 +201,8 @@ public class ReactViewGroup extends ViewGroup @Override public void onRtlPropertiesChanged(int layoutDirection) { - if (mReactBackgroundDrawable != null) { - mReactBackgroundDrawable.setResolvedLayoutDirection(mLayoutDirection); + if (mCSSBackgroundDrawable != null) { + mCSSBackgroundDrawable.setResolvedLayoutDirection(mLayoutDirection); } } @@ -224,7 +225,7 @@ public class ReactViewGroup extends ViewGroup @Override public void setBackgroundColor(int color) { - if (color == Color.TRANSPARENT && mReactBackgroundDrawable == null) { + if (color == Color.TRANSPARENT && mCSSBackgroundDrawable == null) { // don't do anything, no need to allocate ReactBackgroundDrawable for transparent background } else { getOrCreateReactViewBackground().setColor(color); @@ -243,9 +244,9 @@ public class ReactViewGroup extends ViewGroup // as a background previously. This will not work correctly as the drawable callback logic is // messed up in AOSP updateBackgroundDrawable(null); - if (mReactBackgroundDrawable != null && background != null) { + if (mCSSBackgroundDrawable != null && background != null) { LayerDrawable layerDrawable = - new LayerDrawable(new Drawable[] {mReactBackgroundDrawable, background}); + new LayerDrawable(new Drawable[] {mCSSBackgroundDrawable, background}); updateBackgroundDrawable(layerDrawable); } else if (background != null) { updateBackgroundDrawable(background); @@ -332,7 +333,7 @@ public class ReactViewGroup extends ViewGroup backgroundDrawable.setRadius(borderRadius, position); } - public void setBorderRadius(BorderRadiusProp property, @Nullable Float borderRadius) { + public void setBorderRadius(BorderRadiusProp property, @Nullable LengthPercentage borderRadius) { CSSBackgroundDrawable backgroundDrawable = getOrCreateReactViewBackground(); backgroundDrawable.setBorderRadius(property, borderRadius); } @@ -791,26 +792,26 @@ public class ReactViewGroup extends ViewGroup return DEFAULT_BACKGROUND_COLOR; } - /* package */ ReactViewBackgroundDrawable getOrCreateReactViewBackground() { - if (mReactBackgroundDrawable == null) { - mReactBackgroundDrawable = new ReactViewBackgroundDrawable(getContext()); + /* package */ CSSBackgroundDrawable getOrCreateReactViewBackground() { + if (mCSSBackgroundDrawable == null) { + mCSSBackgroundDrawable = new CSSBackgroundDrawable(getContext()); Drawable backgroundDrawable = getBackground(); updateBackgroundDrawable( null); // required so that drawable callback is cleared before we add the // drawable back as a part of LayerDrawable if (backgroundDrawable == null) { - updateBackgroundDrawable(mReactBackgroundDrawable); + updateBackgroundDrawable(mCSSBackgroundDrawable); } else { LayerDrawable layerDrawable = - new LayerDrawable(new Drawable[] {mReactBackgroundDrawable, backgroundDrawable}); + new LayerDrawable(new Drawable[] {mCSSBackgroundDrawable, backgroundDrawable}); updateBackgroundDrawable(layerDrawable); } mLayoutDirection = I18nUtil.getInstance().isRTL(getContext()) ? LAYOUT_DIRECTION_RTL : LAYOUT_DIRECTION_LTR; - mReactBackgroundDrawable.setResolvedLayoutDirection(mLayoutDirection); + mCSSBackgroundDrawable.setResolvedLayoutDirection(mLayoutDirection); } - return mReactBackgroundDrawable; + return mCSSBackgroundDrawable; } @Override @@ -849,7 +850,7 @@ public class ReactViewGroup extends ViewGroup * @param drawable {@link Drawable} The Drawable to use as the background, or null to remove the * background */ - /* package */ void updateBackgroundDrawable(Drawable drawable) { + /* package */ void updateBackgroundDrawable(@Nullable Drawable drawable) { super.setBackground(drawable); } @@ -909,8 +910,8 @@ public class ReactViewGroup extends ViewGroup boolean hasClipPath = false; - if (mReactBackgroundDrawable != null) { - final RectF borderWidth = mReactBackgroundDrawable.getDirectionAwareBorderInsets(); + if (mCSSBackgroundDrawable != null) { + final RectF borderWidth = mCSSBackgroundDrawable.getDirectionAwareBorderInsets(); if (borderWidth.top > 0 || borderWidth.left > 0 @@ -923,7 +924,7 @@ public class ReactViewGroup extends ViewGroup } final ComputedBorderRadius borderRadius = - mReactBackgroundDrawable.getBorderRadius().resolve(mLayoutDirection, getContext()); + mCSSBackgroundDrawable.getComputedBorderRadius(); if (borderRadius.hasRoundedBorders()) { if (mPath == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java index 3d4b23416a2..df4b9c85ce4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java @@ -13,6 +13,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.facebook.common.logging.FLog; import com.facebook.react.bridge.Dynamic; +import com.facebook.react.bridge.DynamicFromObject; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; @@ -21,6 +22,7 @@ import com.facebook.react.common.MapBuilder; import com.facebook.react.common.ReactConstants; import com.facebook.react.common.annotations.VisibleForTesting; import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.uimanager.LengthPercentage; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.PointerEvents; import com.facebook.react.uimanager.Spacing; @@ -30,6 +32,7 @@ import com.facebook.react.uimanager.ViewProps; import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.annotations.ReactPropGroup; import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.uimanager.style.BorderRadiusProp; import com.facebook.yoga.YogaConstants; import java.util.Map; @@ -126,22 +129,19 @@ public class ReactViewManager extends ReactClippingViewManager { ViewProps.BORDER_END_START_RADIUS, ViewProps.BORDER_START_END_RADIUS, ViewProps.BORDER_START_START_RADIUS, - }, - defaultFloat = YogaConstants.UNDEFINED) + }) + public void setBorderRadius(ReactViewGroup view, int index, Dynamic rawBorderRadius) { + + @Nullable LengthPercentage borderRadius = LengthPercentage.setFromDynamic(rawBorderRadius); + + view.setBorderRadius(BorderRadiusProp.values()[index], borderRadius); + } + + /** + * @deprecated Use {@link #setBorderRadius(ReactViewGroup, int, Dynamic)} instead. + */ public void setBorderRadius(ReactViewGroup view, int index, float borderRadius) { - if (!YogaConstants.isUndefined(borderRadius) && borderRadius < 0) { - borderRadius = YogaConstants.UNDEFINED; - } - - if (!YogaConstants.isUndefined(borderRadius)) { - borderRadius = PixelUtil.toPixelFromDIP(borderRadius); - } - - if (index == 0) { - view.setBorderRadius(borderRadius); - } else { - view.setBorderRadius(borderRadius, index - 1); - } + setBorderRadius(view, index, new DynamicFromObject(borderRadius)); } @ReactProp(name = "borderStyle")