Skip to content

Commit da42db9

Browse files
mehulg-metameta-codesync[bot]
authored andcommitted
Fix TextInput outline collapsing below theme default padding (#59372)
Summary: Pull Request resolved: #59372 TextInput keeps the theme's default EditText background but applies the style's padding. That background is an inset around underline nine-patches, so its outline is the view bounds minus the insets, which only the theme padding covers. With less padding, such as `padding: 0`, the outline collapses to a strip through the text, which breaks elevation shadows and anything else that reads the view outline. `ReactEditText` now uses its bounds as the outline in that case, and refreshes the outline when its padding changes, since Android only does so on size and background changes. Any other background, including the ones React Native creates for border and background styles, keeps its normal outline. Changelog: [Android][Fixed] - Fix `TextInput` outline collapsing when its padding is below the theme default Reviewed By: christophpurrer Differential Revision: D126540563 fbshipit-source-id: 3128de816352a84820d521d48b778904008ad5f9
1 parent 6a0f908 commit da42db9

2 files changed

Lines changed: 194 additions & 0 deletions

File tree

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@ import android.content.Context
1212
import android.content.res.Configuration
1313
import android.graphics.Canvas
1414
import android.graphics.Color
15+
import android.graphics.Outline
1516
import android.graphics.Paint
1617
import android.graphics.Rect
1718
import android.graphics.Typeface
19+
import android.graphics.drawable.Drawable
20+
import android.graphics.drawable.InsetDrawable
21+
import android.graphics.drawable.NinePatchDrawable
22+
import android.graphics.drawable.StateListDrawable
1823
import android.os.Build
1924
import android.os.Bundle
2025
import android.text.Editable
@@ -38,6 +43,7 @@ import android.view.MotionEvent
3843
import android.view.View
3944
import android.view.ViewConfiguration
4045
import android.view.ViewGroup
46+
import android.view.ViewOutlineProvider
4147
import android.view.accessibility.AccessibilityNodeInfo
4248
import android.view.inputmethod.EditorInfo
4349
import android.view.inputmethod.InputConnection
@@ -238,6 +244,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
238244
textAttributes = TextAttributes()
239245

240246
applyTextAttributes()
247+
outlineProvider = TextFieldOutlineProvider()
241248

242249
// Turn off hardware acceleration for Oreo (T40484798)
243250
// see https://issuetracker.google.com/issues/67102093
@@ -1379,6 +1386,35 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
13791386
}
13801387
}
13811388

1389+
/**
1390+
* Uses the view bounds as the outline when the padding is below the theme EditText background's
1391+
* own padding, which would otherwise collapse the outline to a strip through the text.
1392+
*/
1393+
private class TextFieldOutlineProvider : ViewOutlineProvider() {
1394+
private val backgroundPadding = Rect()
1395+
1396+
override fun getOutline(view: View, outline: Outline) {
1397+
ViewOutlineProvider.BACKGROUND.getOutline(view, outline)
1398+
val background = view.background ?: return
1399+
if (isThemeTextFieldBackground(background) && hasLessPaddingThan(background, view)) {
1400+
outline.setRect(0, 0, view.width, view.height)
1401+
}
1402+
}
1403+
1404+
private fun isThemeTextFieldBackground(background: Drawable): Boolean {
1405+
val selector = (background as? InsetDrawable)?.drawable as? StateListDrawable
1406+
return selector?.current is NinePatchDrawable
1407+
}
1408+
1409+
private fun hasLessPaddingThan(background: Drawable, view: View): Boolean {
1410+
background.getPadding(backgroundPadding)
1411+
return view.paddingLeft < backgroundPadding.left ||
1412+
view.paddingTop < backgroundPadding.top ||
1413+
view.paddingRight < backgroundPadding.right ||
1414+
view.paddingBottom < backgroundPadding.bottom
1415+
}
1416+
}
1417+
13821418
public companion object {
13831419
public val DEBUG_MODE: Boolean = ReactBuildConfig.DEBUG && false
13841420

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.views.textinput
9+
10+
import android.app.Activity
11+
import android.graphics.Color
12+
import android.graphics.Outline
13+
import android.graphics.Rect
14+
import android.graphics.drawable.Drawable
15+
import android.graphics.drawable.InsetDrawable
16+
import android.graphics.drawable.NinePatchDrawable
17+
import android.graphics.drawable.StateListDrawable
18+
import android.os.Build
19+
import android.os.Looper
20+
import android.util.DisplayMetrics
21+
import android.util.StateSet
22+
import android.view.ContextThemeWrapper
23+
import android.view.View
24+
import android.view.ViewGroup
25+
import android.view.ViewOutlineProvider
26+
import androidx.annotation.RequiresApi
27+
import androidx.core.graphics.drawable.toDrawable
28+
import com.facebook.react.bridge.ReactTestHelper.createCatalystContextForTest
29+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
30+
import com.facebook.react.uimanager.DisplayMetricsHolder
31+
import com.facebook.react.uimanager.ThemedReactContext
32+
import org.assertj.core.api.Assertions.assertThat
33+
import org.junit.Before
34+
import org.junit.Test
35+
import org.junit.runner.RunWith
36+
import org.robolectric.Robolectric
37+
import org.robolectric.RobolectricTestRunner
38+
import org.robolectric.RuntimeEnvironment
39+
import org.robolectric.Shadows.shadowOf
40+
41+
@RunWith(RobolectricTestRunner::class)
42+
@RequiresApi(Build.VERSION_CODES.N)
43+
class ReactEditTextOutlineTest {
44+
45+
private lateinit var view: ReactEditText
46+
47+
@Before
48+
fun setup() {
49+
ReactNativeFeatureFlagsForTests.setUp()
50+
val context = createCatalystContextForTest()
51+
DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics())
52+
view =
53+
ReactTextInputManager().createViewInstance(ThemedReactContext(context, context, null, -1))
54+
view.layout(0, 0, WIDTH, HEIGHT)
55+
}
56+
57+
@Test
58+
fun themeBackground_isInsetSelectorOfNinePatches() {
59+
val background = themeTextFieldBackground()
60+
61+
assertThat(background).isInstanceOf(InsetDrawable::class.java)
62+
val selector = (background as InsetDrawable).drawable
63+
assertThat(selector).isInstanceOf(StateListDrawable::class.java)
64+
assertThat(selector?.current).isInstanceOf(NinePatchDrawable::class.java)
65+
}
66+
67+
@Test
68+
fun themeBackgroundWithoutPadding_usesViewBounds() {
69+
view.background = themeTextFieldBackground()
70+
view.setPadding(0, 0, 0, 0)
71+
72+
assertThat(outlineBounds()).isEqualTo(Rect(0, 0, WIDTH, HEIGHT))
73+
}
74+
75+
@Test
76+
fun themeBackgroundWithOneSideBelowThemePadding_usesViewBounds() {
77+
val background = themeTextFieldBackground()
78+
view.background = background
79+
val padding = Rect().also { background.getPadding(it) }
80+
view.setPadding(padding.left, 0, padding.right, padding.bottom)
81+
82+
assertThat(outlineBounds()).isEqualTo(Rect(0, 0, WIDTH, HEIGHT))
83+
}
84+
85+
@Test
86+
fun themeBackgroundWithThemePadding_usesBackgroundOutline() {
87+
val background = themeTextFieldBackground()
88+
view.background = background
89+
val padding = Rect().also { background.getPadding(it) }
90+
view.setPadding(padding.left, padding.top, padding.right, padding.bottom)
91+
92+
assertThat(outlineBounds()).isEqualTo(backgroundOutlineBounds(background))
93+
}
94+
95+
@Test
96+
fun paddingChangeWhileAttached_rebuildsOutline() {
97+
val background = themeTextFieldBackground()
98+
view.background = background
99+
val padding = Rect().also { background.getPadding(it) }
100+
view.setPadding(padding.left, padding.top, padding.right, padding.bottom)
101+
val rebuiltOutline = Rect()
102+
val provider = view.outlineProvider
103+
view.outlineProvider =
104+
object : ViewOutlineProvider() {
105+
override fun getOutline(view: View, outline: Outline) {
106+
provider.getOutline(view, outline)
107+
outline.getRect(rebuiltOutline)
108+
}
109+
}
110+
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
111+
activity.setContentView(view, ViewGroup.LayoutParams(WIDTH, HEIGHT))
112+
shadowOf(Looper.getMainLooper()).idle()
113+
assertThat(rebuiltOutline).isEqualTo(backgroundOutlineBounds(background))
114+
115+
view.setPadding(0, 0, 0, 0)
116+
117+
assertThat(rebuiltOutline).isEqualTo(Rect(0, 0, WIDTH, HEIGHT))
118+
}
119+
120+
@Test
121+
fun customInsetBackgroundWithoutPadding_usesBackgroundOutline() {
122+
val selector = StateListDrawable()
123+
selector.addState(StateSet.WILD_CARD, Color.BLACK.toDrawable())
124+
val background = InsetDrawable(selector, 4, 10, 4, 7)
125+
background.setBounds(0, 0, WIDTH, HEIGHT)
126+
view.background = background
127+
view.setPadding(0, 0, 0, 0)
128+
129+
assertThat(outlineBounds()).isEqualTo(Rect(4, 10, WIDTH - 4, HEIGHT - 7))
130+
}
131+
132+
private fun themeTextFieldBackground(): Drawable {
133+
val context =
134+
ContextThemeWrapper(RuntimeEnvironment.getApplication(), android.R.style.Theme_Material)
135+
val attrs = context.obtainStyledAttributes(intArrayOf(android.R.attr.editTextBackground))
136+
val background = checkNotNull(attrs.getDrawable(0))
137+
attrs.recycle()
138+
background.setBounds(0, 0, WIDTH, HEIGHT)
139+
return background
140+
}
141+
142+
private fun outlineBounds(): Rect {
143+
val outline = Outline()
144+
view.outlineProvider.getOutline(view, outline)
145+
return Rect().also { outline.getRect(it) }
146+
}
147+
148+
private fun backgroundOutlineBounds(background: Drawable): Rect {
149+
val outline = Outline()
150+
background.getOutline(outline)
151+
return Rect().also { outline.getRect(it) }
152+
}
153+
154+
private companion object {
155+
const val WIDTH = 200
156+
const val HEIGHT = 100
157+
}
158+
}

0 commit comments

Comments
 (0)