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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import android.os.Build
import android.os.Bundle
import android.text.Editable
import android.text.InputType
import android.text.Layout
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.Spanned
Expand Down Expand Up @@ -1239,31 +1240,44 @@ public open class ReactEditText public constructor(context: Context) : AppCompat

val spanned = text as? Spanned
if (spanned != null) {
val layout = layout
val drawSpans = spanned.getSpans(0, spanned.length, CanvasEffectSpan::class.java)
val layout = if (drawSpans.isNotEmpty()) layoutForDraw() else null
if (layout != null) {
val drawSpans = spanned.getSpans(0, spanned.length, CanvasEffectSpan::class.java)
if (drawSpans.isNotEmpty()) {
canvas.withTranslation(compoundPaddingLeft.toFloat(), extendedPaddingTop.toFloat()) {
for (span in drawSpans) {
span.onPreDraw(spanned.getSpanStart(span), spanned.getSpanEnd(span), this, layout)
}
canvas.withTranslation(compoundPaddingLeft.toFloat(), extendedPaddingTop.toFloat()) {
for (span in drawSpans) {
span.onPreDraw(spanned.getSpanStart(span), spanned.getSpanEnd(span), this, layout)
}
}

super.onDraw(canvas)
super.onDraw(canvas)

canvas.withTranslation(compoundPaddingLeft.toFloat(), extendedPaddingTop.toFloat()) {
for (span in drawSpans) {
span.onDraw(spanned.getSpanStart(span), spanned.getSpanEnd(span), this, layout)
}
canvas.withTranslation(compoundPaddingLeft.toFloat(), extendedPaddingTop.toFloat()) {
for (span in drawSpans) {
span.onDraw(spanned.getSpanStart(span), spanned.getSpanEnd(span), this, layout)
}
return
}
return
}
}

super.onDraw(canvas)
}

/**
* TextView drops its layout when an attribute like the placeholder changes, and Fabric only
* measures this view again when its frame changes. Rebuild the layout for the current frame so
* the CanvasEffectSpans drawn around super.onDraw are not skipped.
*/
private fun layoutForDraw(): Layout? {
if (layout == null && width > 0) {
measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY),
)
}
return layout
}

public override fun onDragEvent(event: DragEvent): Boolean {
val dragFilter = dragAndDropFilter
if (dragFilter != null && event.action == DragEvent.ACTION_DRAG_STARTED) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* 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.
*/

@file:Suppress("DEPRECATION")

package com.facebook.react.views.textinput

import android.graphics.Canvas
import android.text.Layout
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.util.DisplayMetrics
import android.view.View
import android.view.ViewGroup
import androidx.core.content.res.ResourcesCompat.ID_NULL
import androidx.core.graphics.createBitmap
import com.facebook.react.bridge.BridgeReactContext
import com.facebook.react.bridge.ReactTestHelper.createMockCatalystInstance
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
import com.facebook.react.uimanager.DisplayMetricsHolder
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.views.text.internal.span.CanvasEffectSpan
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)
class ReactEditTextTest {

private lateinit var view: ReactEditText

@Before
fun setUp() {
ReactNativeFeatureFlagsForTests.setUp()
val context = BridgeReactContext(RuntimeEnvironment.getApplication())
context.initializeWithInstance(createMockCatalystInstance())
val themedContext = ThemedReactContext(context, context.baseContext, null, ID_NULL)
DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics())
view = ReactTextInputManager().createViewInstance(themedContext)
}

@Test
fun drawsCanvasEffectSpansAfterPlaceholderChangeThatKeepsTheFrame() {
val width = 200
val height = 40
val text = SpannableStringBuilder("Underlined")
val span = RecordingCanvasEffectSpan()
text.setSpan(span, 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

view.layoutParams =
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
view.setText(text)
view.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY),
)
view.layout(0, 0, width, height)
view.draw(Canvas(createBitmap(width, height)))
span.reset()

// Changing the placeholder drops TextView's layout, but Fabric does not measure the view again
// because its frame did not change.
view.setPlaceholder("Placeholder")
view.draw(Canvas(createBitmap(width, height)))

assertThat(span.preDrawCount).isEqualTo(1)
assertThat(span.drawCount).isEqualTo(1)
}

private class RecordingCanvasEffectSpan : CanvasEffectSpan() {
var preDrawCount = 0
var drawCount = 0

fun reset() {
preDrawCount = 0
drawCount = 0
}

override fun onPreDraw(start: Int, end: Int, canvas: Canvas, layout: Layout) {
preDrawCount++
}

override fun onDraw(start: Int, end: Int, canvas: Canvas, layout: Layout) {
drawCount++
}
}
}
Loading