From fd126b36b7fb18c2be5233d223d7b818419c628d Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Thu, 24 Apr 2025 17:20:03 -0700 Subject: [PATCH] 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 --- .../text/PreparedLayoutTextViewManager.kt | 219 ++++++++++++++++++ .../ReactTextViewAccessibilityDelegate.kt | 2 +- 2 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextViewManager.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextViewManager.kt new file mode 100644 index 00000000000..df5fbc29cc2 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextViewManager.kt @@ -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(), + IViewGroupManager { + + 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? { + 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 { + 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" + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewAccessibilityDelegate.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewAccessibilityDelegate.kt index 2f207e49178..3e686ac2126 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewAccessibilityDelegate.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewAccessibilityDelegate.kt @@ -270,7 +270,7 @@ internal class ReactTextViewAccessibilityDelegate : ReactAccessibilityDelegate { return null } - public class AccessibilityLinks(spans: Array, text: Spannable) { + public class AccessibilityLinks(spans: Array, text: Spannable) { private val links: List init {