Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -6292,6 +6292,7 @@ public class com/facebook/react/views/textinput/ReactEditText : androidx/appcomp
protected final fun setNativeEventCount (I)V
public final fun setOnKeyPress (Z)V
public final fun setOverflow (Ljava/lang/String;)V
public fun setPadding (IIII)V
public final fun setPlaceholder (Ljava/lang/String;)V
public final fun setReturnKeyType (Ljava/lang/String;)V
public final fun setScrollWatcher (Lcom/facebook/react/views/textinput/ScrollWatcher;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ import android.content.Context
import android.content.res.Configuration
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Outline
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.graphics.drawable.InsetDrawable
import android.graphics.drawable.NinePatchDrawable
import android.graphics.drawable.StateListDrawable
import android.os.Build
import android.os.Bundle
import android.text.Editable
Expand All @@ -37,6 +42,7 @@ import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewOutlineProvider
import android.view.accessibility.AccessibilityNodeInfo
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
Expand Down Expand Up @@ -223,6 +229,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
textAttributes = TextAttributes()

applyTextAttributes()
outlineProvider = TextFieldOutlineProvider()

// Turn off hardware acceleration for Oreo (T40484798)
// see https://issuetracker.google.com/issues/67102093
Expand Down Expand Up @@ -1058,6 +1065,16 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
didAttachToWindow = true
}

override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
val isSamePadding =
left == paddingLeft && top == paddingTop && right == paddingRight && bottom == paddingBottom
super.setPadding(left, top, right, bottom)
// Android doesn't rebuild the outline on padding changes.
if (!isSamePadding) {
invalidateOutline()
}
}

override fun setBackgroundColor(color: Int) {
setBackgroundColor(this, color)
}
Expand Down Expand Up @@ -1346,6 +1363,35 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
}
}

/**
* Uses the view bounds as the outline when the padding is below the theme EditText background's
* own padding, which would otherwise collapse the outline to a strip through the text.
*/
private class TextFieldOutlineProvider : ViewOutlineProvider() {
private val backgroundPadding = Rect()

override fun getOutline(view: View, outline: Outline) {
ViewOutlineProvider.BACKGROUND.getOutline(view, outline)
val background = view.background ?: return
if (isThemeTextFieldBackground(background) && hasLessPaddingThan(background, view)) {
outline.setRect(0, 0, view.width, view.height)
}
}

private fun isThemeTextFieldBackground(background: Drawable): Boolean {
val selector = (background as? InsetDrawable)?.drawable as? StateListDrawable
return selector?.current is NinePatchDrawable
}

private fun hasLessPaddingThan(background: Drawable, view: View): Boolean {
background.getPadding(backgroundPadding)
return view.paddingLeft < backgroundPadding.left ||
view.paddingTop < backgroundPadding.top ||
view.paddingRight < backgroundPadding.right ||
view.paddingBottom < backgroundPadding.bottom
}
}

public companion object {
public val DEBUG_MODE: Boolean = ReactBuildConfig.DEBUG && false

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* 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.views.textinput

import android.graphics.Color
import android.graphics.Outline
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.graphics.drawable.InsetDrawable
import android.graphics.drawable.NinePatchDrawable
import android.graphics.drawable.StateListDrawable
import android.os.Build
import android.util.DisplayMetrics
import android.util.StateSet
import android.view.ContextThemeWrapper
import androidx.annotation.RequiresApi
import androidx.core.graphics.drawable.toDrawable
import com.facebook.react.bridge.ReactTestHelper.createCatalystContextForTest
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
import com.facebook.react.uimanager.DisplayMetricsHolder
import com.facebook.react.uimanager.ThemedReactContext
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment

@RunWith(RobolectricTestRunner::class)
@RequiresApi(Build.VERSION_CODES.N)
class ReactEditTextOutlineTest {

private lateinit var view: ReactEditText

@Before
fun setup() {
ReactNativeFeatureFlagsForTests.setUp()
val context = createCatalystContextForTest()
DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics())
view =
ReactTextInputManager().createViewInstance(ThemedReactContext(context, context, null, -1))
view.layout(0, 0, WIDTH, HEIGHT)
}

@Test
fun themeBackground_isInsetSelectorOfNinePatches() {
val background = themeTextFieldBackground()

assertThat(background).isInstanceOf(InsetDrawable::class.java)
val selector = (background as InsetDrawable).drawable
assertThat(selector).isInstanceOf(StateListDrawable::class.java)
assertThat(selector?.current).isInstanceOf(NinePatchDrawable::class.java)
}

@Test
fun themeBackgroundWithoutPadding_usesViewBounds() {
view.background = themeTextFieldBackground()
view.setPadding(0, 0, 0, 0)

assertThat(outlineBounds()).isEqualTo(Rect(0, 0, WIDTH, HEIGHT))
}

@Test
fun themeBackgroundWithOneSideBelowThemePadding_usesViewBounds() {
val background = themeTextFieldBackground()
view.background = background
val padding = Rect().also { background.getPadding(it) }
view.setPadding(padding.left, 0, padding.right, padding.bottom)

assertThat(outlineBounds()).isEqualTo(Rect(0, 0, WIDTH, HEIGHT))
}

@Test
fun themeBackgroundWithThemePadding_usesBackgroundOutline() {
val background = themeTextFieldBackground()
view.background = background
val padding = Rect().also { background.getPadding(it) }
view.setPadding(padding.left, padding.top, padding.right, padding.bottom)

assertThat(outlineBounds()).isEqualTo(backgroundOutlineBounds(background))
}

@Test
fun customInsetBackgroundWithoutPadding_usesBackgroundOutline() {
val selector = StateListDrawable()
selector.addState(StateSet.WILD_CARD, Color.BLACK.toDrawable())
val background = InsetDrawable(selector, 4, 10, 4, 7)
background.setBounds(0, 0, WIDTH, HEIGHT)
view.background = background
view.setPadding(0, 0, 0, 0)

assertThat(outlineBounds()).isEqualTo(Rect(4, 10, WIDTH - 4, HEIGHT - 7))
}

private fun themeTextFieldBackground(): Drawable {
val context =
ContextThemeWrapper(RuntimeEnvironment.getApplication(), android.R.style.Theme_Material)
val attrs = context.obtainStyledAttributes(intArrayOf(android.R.attr.editTextBackground))
val background = checkNotNull(attrs.getDrawable(0))
attrs.recycle()
background.setBounds(0, 0, WIDTH, HEIGHT)
return background
}

private fun outlineBounds(): Rect {
val outline = Outline()
view.outlineProvider.getOutline(view, outline)
return Rect().also { outline.getRect(it) }
}

private fun backgroundOutlineBounds(background: Drawable): Rect {
val outline = Outline()
background.getOutline(outline)
return Rect().also { outline.getRect(it) }
}

private companion object {
const val WIDTH = 200
const val HEIGHT = 100
}
}
Loading