From 049012bf9b4fc2422ec0d785c05644290c23e99b Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Thu, 22 Oct 2026 22:40:55 +0200 Subject: [PATCH] Use a round rect outline for Views with four equal corners on Android Co-Authored-By: Claude Opus 5.5 --- .../drawable/CompositeBackgroundDrawable.kt | 22 ++-- .../CompositeBackgroundDrawableTest.kt | 100 ++++++++++++++++++ 2 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/drawable/CompositeBackgroundDrawableTest.kt 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 335ea16bdd63..626d7286418e 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 @@ -187,8 +187,6 @@ internal class CompositeBackgroundDrawable( elevation shadow. */ override fun getOutline(outline: Outline) { if (borderRadius?.hasRoundedBorders() == true) { - val pathForOutline = Path() - val computedBorderRadius = borderRadius?.resolve( layoutDirection, @@ -199,9 +197,8 @@ internal class CompositeBackgroundDrawable( val computedBorderInsets = borderInsets?.resolve(layoutDirection, context) - computedBorderRadius?.let { - pathForOutline.addRoundRect( - RectF(bounds), + val radii = + computedBorderRadius?.let { floatArrayOf( (it.topLeft.horizontal + (computedBorderInsets?.left ?: 0f)).dpToPx(), (it.topLeft.vertical + (computedBorderInsets?.top ?: 0f)).dpToPx(), @@ -211,11 +208,20 @@ internal class CompositeBackgroundDrawable( (it.bottomRight.vertical + (computedBorderInsets?.bottom ?: 0f)).dpToPx(), (it.bottomLeft.horizontal + (computedBorderInsets?.left ?: 0f)).dpToPx(), (it.bottomLeft.vertical + (computedBorderInsets?.bottom ?: 0f)).dpToPx(), - ), - Path.Direction.CW, - ) + ) + } + + // Four equal circular corners describe the same shape as the rounded path below, without + // building a path. Like the path, a radius larger than half the shorter side is reduced to + // it. + if (radii != null && radii.all { it == radii[0] }) { + outline.setRoundRect(bounds, minOf(radii[0], minOf(bounds.width(), bounds.height()) / 2f)) + return } + val pathForOutline = Path() + radii?.let { pathForOutline.addRoundRect(RectF(bounds), it, Path.Direction.CW) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { outline.setPath(pathForOutline) } else { diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/drawable/CompositeBackgroundDrawableTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/drawable/CompositeBackgroundDrawableTest.kt new file mode 100644 index 000000000000..1aa53a915789 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/drawable/CompositeBackgroundDrawableTest.kt @@ -0,0 +1,100 @@ +/* + * 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.Outline +import android.graphics.Rect +import android.util.DisplayMetrics +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import com.facebook.react.uimanager.DisplayMetricsHolder +import com.facebook.react.uimanager.LengthPercentage +import com.facebook.react.uimanager.LengthPercentageType +import com.facebook.react.uimanager.style.BorderRadiusProp +import com.facebook.react.uimanager.style.BorderRadiusStyle +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +/** Tests for the outline of [CompositeBackgroundDrawable] */ +@RunWith(RobolectricTestRunner::class) +class CompositeBackgroundDrawableTest { + + private val context: Context = RuntimeEnvironment.getApplication() + + @Before + fun setUp() { + ReactNativeFeatureFlagsForTests.setUp() + DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics().apply { density = 2f }) + } + + @After + fun tearDown() { + DisplayMetricsHolder.setScreenDisplayMetrics(null) + ReactNativeFeatureFlags.dangerouslyReset() + } + + @Test + fun equalCornersGiveARoundRect() { + val outline = + outlineOf(BorderRadiusStyle(listOf(BorderRadiusProp.BORDER_RADIUS to points(10f)))) + + val rect = Rect() + assertThat(outline.getRect(rect)).isTrue() + assertThat(rect).isEqualTo(Rect(0, 0, 200, 100)) + assertThat(outline.radius).isEqualTo(20f) + } + + @Test + fun radiusLargerThanHalfTheShorterSideIsReducedToIt() { + val outline = + outlineOf(BorderRadiusStyle(listOf(BorderRadiusProp.BORDER_RADIUS to points(80f)))) + + assertThat(outline.getRect(Rect())).isTrue() + assertThat(outline.radius).isEqualTo(50f) + } + + @Test + fun differentCornersGiveAPath() { + val outline = + outlineOf( + BorderRadiusStyle( + listOf( + BorderRadiusProp.BORDER_RADIUS to points(10f), + BorderRadiusProp.BORDER_TOP_LEFT_RADIUS to points(4f), + ) + ) + ) + + assertThat(outline.isEmpty).isFalse() + assertThat(outline.getRect(Rect())).isFalse() + } + + @Test + fun noRoundedCornersGiveARect() { + val outline = outlineOf(null) + + val rect = Rect() + assertThat(outline.getRect(rect)).isTrue() + assertThat(rect).isEqualTo(Rect(0, 0, 200, 100)) + assertThat(outline.radius).isEqualTo(0f) + } + + private fun points(value: Float) = LengthPercentage(value, LengthPercentageType.POINT) + + private fun outlineOf(borderRadius: BorderRadiusStyle?): Outline { + val drawable = CompositeBackgroundDrawable(context = context, borderRadius = borderRadius) + drawable.setBounds(0, 0, 200, 100) + return Outline().also { drawable.getOutline(it) } + } +}