PreparedLayoutTextViewManager (#50923)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/50923

This implements the view manager for `PreparedLayoutTextView`, originating by taking the view managers composing `ReactTextView`, converting to Kotlin, and removing everything no longer needed.

In Facsimile, anything influencing text appearance is applied earlier, when creating the Fabric layout, so there are many less setters here. Most visual attributes are instead present in the state we are presenting.

We have tasks for some of these, that need to be reimplemented, as they do not currently influence the Spannable being measured. That includes e.g. `ReactTextViewManagerCallback`, used for injection, and `dataDetectorType` for linkifying Spannable.

Changelog: [Internal]

Reviewed By: rshest

Differential Revision: D73287706

fbshipit-source-id: 938b57d4e443f6b8bb127e17b47cc371f31a416d
This commit is contained in:
Nick Gerleman
2025-04-24 17:20:03 -07:00
committed by Facebook GitHub Bot
parent 6465cb1b0b
commit fd126b36b7
2 changed files with 220 additions and 1 deletions
@@ -0,0 +1,219 @@
/*
* 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.text
import android.text.Layout
import android.text.Spannable
import android.view.View
import com.facebook.react.R
import com.facebook.react.internal.SystraceSection
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.BackgroundStyleApplicator
import com.facebook.react.uimanager.BaseViewManager
import com.facebook.react.uimanager.IViewGroupManager
import com.facebook.react.uimanager.LayoutShadowNode
import com.facebook.react.uimanager.LengthPercentage
import com.facebook.react.uimanager.LengthPercentageType
import com.facebook.react.uimanager.ReactStylesDiffMap
import com.facebook.react.uimanager.ReferenceStateWrapper
import com.facebook.react.uimanager.StateWrapper
import com.facebook.react.uimanager.ThemedReactContext
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.style.BorderRadiusProp
import com.facebook.react.uimanager.style.BorderStyle
import com.facebook.react.uimanager.style.LogicalEdge
import com.facebook.react.uimanager.style.Overflow
import com.facebook.react.views.text.ReactTextViewAccessibilityDelegate.AccessibilityLinks
import com.facebook.react.views.text.internal.span.ReactClickableSpan
import java.util.HashMap
@ReactModule(name = PreparedLayoutTextViewManager.REACT_CLASS)
internal class PreparedLayoutTextViewManager :
BaseViewManager<PreparedLayoutTextView, LayoutShadowNode>(),
IViewGroupManager<PreparedLayoutTextView> {
init {
setupViewRecycling()
}
override fun prepareToRecycleView(
reactContext: ThemedReactContext,
view: PreparedLayoutTextView
): PreparedLayoutTextView? {
val preparedView = super.prepareToRecycleView(reactContext, view)
preparedView?.recycleView()
return preparedView
}
override fun getName(): String = REACT_CLASS
override fun updateViewAccessibility(view: PreparedLayoutTextView) {
ReactTextViewAccessibilityDelegate.setDelegate(
view, view.isFocusable, view.importantForAccessibility)
}
public override fun createViewInstance(context: ThemedReactContext): PreparedLayoutTextView =
PreparedLayoutTextView(context)
override fun updateExtraData(view: PreparedLayoutTextView, extraData: Any) {
SystraceSection("PreparedLayoutTextViewManager.updateExtraData").use { _ ->
val layout = extraData as Layout
view.layout = layout
// If this text view contains any clickable spans, set a view tag and reset the accessibility
// delegate so that these can be picked up by the accessibility system.
if (layout.text is Spannable) {
val spannableText = layout.text as Spannable
val clickableSpans =
spannableText.getSpans(0, layout.text.length, ReactClickableSpan::class.java)
view.setTag(
R.id.accessibility_links,
if (clickableSpans.size > 0) AccessibilityLinks(clickableSpans, spannableText)
else null)
ReactTextViewAccessibilityDelegate.resetDelegate(
view, view.isFocusable, view.importantForAccessibility)
}
}
}
override fun updateState(
view: PreparedLayoutTextView,
props: ReactStylesDiffMap,
stateWrapper: StateWrapper
): Any? = (stateWrapper as? ReferenceStateWrapper)?.stateDataReference
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any>? {
val baseEventTypeConstants = super.getExportedCustomDirectEventTypeConstants()
val eventTypeConstants = baseEventTypeConstants ?: HashMap()
eventTypeConstants.put("topTextLayout", mapOf("registrationName" to "onTextLayout"))
return eventTypeConstants
}
@ReactProp(name = "overflow")
public fun setOverflow(view: PreparedLayoutTextView, overflow: String?): Unit {
view.overflow = overflow?.let { Overflow.fromString(it) } ?: Overflow.HIDDEN
}
@ReactProp(name = "accessible")
public fun setAccessible(view: PreparedLayoutTextView, accessible: Boolean): Unit {
view.isFocusable = accessible
}
@ReactProp(name = "selectable", defaultBoolean = false)
public fun setSelectable(view: PreparedLayoutTextView, isSelectable: Boolean): Unit {
// T222052152: Implement fine-grained text selection for PreparedLayoutTextView
// view.setTextIsSelectable(isSelectable);
}
@ReactProp(name = "selectionColor", customType = "Color")
public fun setSelectionColor(view: PreparedLayoutTextView, color: Int?): Unit {
if (color == null) {
view.selectionColor = DefaultStyleValuesUtil.getDefaultTextColorHighlight(view.context)
} else {
view.selectionColor = color
}
}
@ReactPropGroup(
names =
[
ViewProps.BORDER_RADIUS,
ViewProps.BORDER_TOP_LEFT_RADIUS,
ViewProps.BORDER_TOP_RIGHT_RADIUS,
ViewProps.BORDER_BOTTOM_RIGHT_RADIUS,
ViewProps.BORDER_BOTTOM_LEFT_RADIUS],
defaultFloat = Float.NaN)
public fun setBorderRadius(view: PreparedLayoutTextView, index: Int, borderRadius: Float): Unit {
val radius =
if (borderRadius.isNaN()) null
else LengthPercentage(borderRadius, LengthPercentageType.POINT)
BackgroundStyleApplicator.setBorderRadius(view, BorderRadiusProp.values()[index], radius)
}
@ReactProp(name = "borderStyle")
public fun setBorderStyle(view: PreparedLayoutTextView, borderStyle: String?): Unit {
val parsedBorderStyle = if (borderStyle == null) null else BorderStyle.fromString(borderStyle)
BackgroundStyleApplicator.setBorderStyle(view, parsedBorderStyle)
}
@ReactPropGroup(
names =
[
ViewProps.BORDER_WIDTH,
ViewProps.BORDER_LEFT_WIDTH,
ViewProps.BORDER_RIGHT_WIDTH,
ViewProps.BORDER_TOP_WIDTH,
ViewProps.BORDER_BOTTOM_WIDTH,
ViewProps.BORDER_START_WIDTH,
ViewProps.BORDER_END_WIDTH],
defaultFloat = Float.NaN)
public fun setBorderWidth(view: PreparedLayoutTextView, index: Int, width: Float): Unit {
BackgroundStyleApplicator.setBorderWidth(view, LogicalEdge.values()[index], width)
}
@ReactPropGroup(
names =
[
ViewProps.BORDER_COLOR,
ViewProps.BORDER_LEFT_COLOR,
ViewProps.BORDER_RIGHT_COLOR,
ViewProps.BORDER_TOP_COLOR,
ViewProps.BORDER_BOTTOM_COLOR,
ViewProps.BORDER_START_COLOR,
ViewProps.BORDER_END_COLOR,
ViewProps.BORDER_BLOCK_COLOR,
ViewProps.BORDER_BLOCK_END_COLOR,
ViewProps.BORDER_BLOCK_START_COLOR,
],
customType = "Color")
public fun setBorderColor(view: PreparedLayoutTextView, index: Int, color: Int?): Unit {
BackgroundStyleApplicator.setBorderColor(view, LogicalEdge.values()[index], color)
}
@ReactProp(name = "disabled", defaultBoolean = false)
public fun setDisabled(view: PreparedLayoutTextView, disabled: Boolean): Unit {
view.setEnabled(!disabled)
}
override fun setPadding(
view: PreparedLayoutTextView,
left: Int,
top: Int,
right: Int,
bottom: Int
): Unit {
view.setPadding(left, top, right, bottom)
}
override fun getShadowNodeClass(): Class<out LayoutShadowNode> {
throw UnsupportedOperationException(
"PreparedLayoutTextViewManager does not use legacy arch shadow nodes")
}
override fun addView(parent: PreparedLayoutTextView, child: View, index: Int) {
parent.addView(child, index)
}
override fun getChildAt(parent: PreparedLayoutTextView, index: Int): View? =
parent.getChildAt(index)
override fun removeViewAt(parent: PreparedLayoutTextView, index: Int) {
parent.removeViewAt(index)
}
override fun getChildCount(parent: PreparedLayoutTextView): Int = parent.childCount
override fun needsCustomLayoutForChildren(): Boolean = false
public companion object {
public const val REACT_CLASS: String = "RCTText"
}
}
@@ -270,7 +270,7 @@ internal class ReactTextViewAccessibilityDelegate : ReactAccessibilityDelegate {
return null
}
public class AccessibilityLinks(spans: Array<ClickableSpan?>, text: Spannable) {
public class AccessibilityLinks(spans: Array<out ClickableSpan>, text: Spannable) {
private val links: List<AccessibleLink>
init {