Convert to Kotlin - ReactTextInputShadowNode (#50648)

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

This diff converts another class to Kotlin - ReactTextInputShadowNode

Changelog:
[Internal] [Changed] - ReactTextInputShadowNode to Kotlin

Reviewed By: rshest

Differential Revision: D72854819

fbshipit-source-id: 94d2f4e05ecdbf0d7507e59a0e11fd02d4dbfa55
This commit is contained in:
Nicola Corti
2025-04-11 08:09:24 -07:00
committed by Facebook GitHub Bot
parent 798eb0ab84
commit 45e725b3dc
4 changed files with 215 additions and 239 deletions
@@ -6891,6 +6891,9 @@ public class com/facebook/react/views/textinput/ReactTextInputManager : com/face
public fun updateState (Lcom/facebook/react/views/textinput/ReactEditText;Lcom/facebook/react/uimanager/ReactStylesDiffMap;Lcom/facebook/react/uimanager/StateWrapper;)Ljava/lang/Object;
}
public final class com/facebook/react/views/textinput/ReactTextInputShadowNode$Companion {
}
public abstract interface class com/facebook/react/views/textinput/ScrollWatcher {
public abstract fun onScrollChanged (IIII)V
}
@@ -247,7 +247,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
ReactBaseTextShadowNode textShadowNode,
@Nullable String text,
boolean supportsInlineViews,
NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) {
@Nullable NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) {
Assertions.assertCondition(
!supportsInlineViews || nativeViewHierarchyOptimizer != null,
"nativeViewHierarchyOptimizer is required when inline views are supported");
@@ -296,6 +296,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
Assertions.assertNotNull(inlineViews).get(placeholder.getReactTag());
Assertions.assertNotNull(childNode);
Assertions.assertNotNull(nativeViewHierarchyOptimizer);
nativeViewHierarchyOptimizer.handleForceViewToBeNonLayoutOnly(childNode);
// The ReactTextView is responsible for laying out the inline views.
@@ -1,238 +0,0 @@
/*
* 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.text.Layout;
import android.util.TypedValue;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.annotation.Nullable;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.core.view.ViewCompat;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.R;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.common.annotations.internal.LegacyArchitecture;
import com.facebook.react.uimanager.Spacing;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.UIViewOperationQueue;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.views.text.ReactBaseTextShadowNode;
import com.facebook.react.views.text.ReactTextUpdate;
import com.facebook.react.views.text.ReactTextViewManagerCallback;
import com.facebook.react.views.view.MeasureUtil;
import com.facebook.yoga.YogaMeasureFunction;
import com.facebook.yoga.YogaMeasureMode;
import com.facebook.yoga.YogaMeasureOutput;
import com.facebook.yoga.YogaNode;
@VisibleForTesting
@LegacyArchitecture
public class ReactTextInputShadowNode extends ReactBaseTextShadowNode
implements YogaMeasureFunction {
private int mMostRecentEventCount = ReactConstants.UNSET;
private @Nullable EditText mInternalEditText;
private @Nullable ReactTextInputLocalData mLocalData;
@VisibleForTesting public static final String PROP_TEXT = "text";
@VisibleForTesting public static final String PROP_PLACEHOLDER = "placeholder";
// Represents the {@code text} property only, not possible nested content.
private @Nullable String mText = null;
private @Nullable String mPlaceholder = null;
public ReactTextInputShadowNode(
@Nullable ReactTextViewManagerCallback reactTextViewManagerCallback) {
super(reactTextViewManagerCallback);
mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
initMeasureFunction();
}
public ReactTextInputShadowNode() {
this(null);
}
private void initMeasureFunction() {
setMeasureFunction(this);
}
@Override
public void setThemedContext(ThemedReactContext themedContext) {
super.setThemedContext(themedContext);
// {@code EditText} has by default a border at the bottom of its view
// called "underline". To have a native look and feel of the TextEdit
// we have to preserve it at least by default.
// The border (underline) has its padding set by the background image
// provided by the system (which vary a lot among versions and vendors
// of Android), and it cannot be changed.
// So, we have to enforce it as a default padding.
// TODO #7120264: Cache this stuff better.
EditText editText = createInternalEditText();
setDefaultPadding(Spacing.START, ViewCompat.getPaddingStart(editText));
setDefaultPadding(Spacing.TOP, editText.getPaddingTop());
setDefaultPadding(Spacing.END, ViewCompat.getPaddingEnd(editText));
setDefaultPadding(Spacing.BOTTOM, editText.getPaddingBottom());
mInternalEditText = editText;
// We must measure the EditText without paddings, so we have to reset them.
mInternalEditText.setPadding(0, 0, 0, 0);
// This is needed to fix an android bug since 4.4.3 which will throw an NPE in measure,
// setting the layoutParams fixes it: https://code.google.com/p/android/issues/detail?id=75877
mInternalEditText.setLayoutParams(
new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
@Override
public long measure(
YogaNode node,
float width,
YogaMeasureMode widthMode,
float height,
YogaMeasureMode heightMode) {
// measure() should never be called before setThemedContext()
EditText editText = Assertions.assertNotNull(mInternalEditText);
if (mLocalData != null) {
mLocalData.apply(editText);
} else {
editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextAttributes.getEffectiveFontSize());
if (mNumberOfLines != ReactConstants.UNSET) {
editText.setLines(mNumberOfLines);
}
if (editText.getBreakStrategy() != mTextBreakStrategy) {
editText.setBreakStrategy(mTextBreakStrategy);
}
}
// make sure the placeholder content is also being measured
editText.setHint(getPlaceholder());
editText.measure(
MeasureUtil.getMeasureSpec(width, widthMode),
MeasureUtil.getMeasureSpec(height, heightMode));
return YogaMeasureOutput.make(editText.getMeasuredWidth(), editText.getMeasuredHeight());
}
@Override
public boolean isVirtualAnchor() {
return true;
}
@Override
public boolean isYogaLeafNode() {
return true;
}
@Override
public void setLocalData(Object data) {
Assertions.assertCondition(data instanceof ReactTextInputLocalData);
mLocalData = (ReactTextInputLocalData) data;
// Telling to Yoga that the node should be remeasured on next layout pass.
dirty();
// Note: We should NOT mark the node updated (by calling {@code markUpdated}) here
// because the state remains the same.
}
@ReactProp(name = "mostRecentEventCount")
public void setMostRecentEventCount(int mostRecentEventCount) {
mMostRecentEventCount = mostRecentEventCount;
}
@ReactProp(name = PROP_TEXT)
public void setText(@Nullable String text) {
mText = text;
markUpdated();
}
public @Nullable String getText() {
return mText;
}
@ReactProp(name = PROP_PLACEHOLDER)
public void setPlaceholder(@Nullable String placeholder) {
mPlaceholder = placeholder;
markUpdated();
}
public @Nullable String getPlaceholder() {
return mPlaceholder;
}
@Override
public void setTextBreakStrategy(@Nullable String textBreakStrategy) {
if (textBreakStrategy == null || "simple".equals(textBreakStrategy)) {
mTextBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
} else if ("highQuality".equals(textBreakStrategy)) {
mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
} else if ("balanced".equals(textBreakStrategy)) {
mTextBreakStrategy = Layout.BREAK_STRATEGY_BALANCED;
} else {
FLog.w(ReactConstants.TAG, "Invalid textBreakStrategy: " + textBreakStrategy);
mTextBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
}
}
@Override
public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
super.onCollectExtraUpdates(uiViewOperationQueue);
if (mMostRecentEventCount != ReactConstants.UNSET) {
ReactTextUpdate reactTextUpdate =
new ReactTextUpdate(
spannedFromShadowNode(
this,
getText(),
/* supportsInlineViews: */ false,
/* nativeViewHierarchyOptimizer: */ null // only needed to support inline views
),
mMostRecentEventCount,
mContainsImages,
getPadding(Spacing.LEFT),
getPadding(Spacing.TOP),
getPadding(Spacing.RIGHT),
getPadding(Spacing.BOTTOM),
mTextAlign,
mTextBreakStrategy,
mJustificationMode);
uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate);
}
}
@Override
public void setPadding(int spacingType, float padding) {
super.setPadding(spacingType, padding);
markUpdated();
}
/**
* May be overridden by subclasses that would like to provide their own instance of the internal
* {@code EditText} this class uses to determine the expected size of the view.
*/
protected EditText createInternalEditText() {
// By setting a style which has a background drawable, this EditText will have a different
// background drawable instance from that on the UI Thread, which maybe has a default background
// drawable instance.
// Otherwise, DrawableContainer is not a thread safe class, and it caused the npe in #29452.
ContextThemeWrapper context =
new ContextThemeWrapper(
getThemedContext(), R.style.Theme_ReactNative_TextInput_DefaultBackground);
return new EditText(context);
}
}
@@ -0,0 +1,210 @@
/*
* 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.annotation.SuppressLint
import android.text.Layout
import android.util.TypedValue
import android.view.ViewGroup
import android.widget.EditText
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.ViewCompat
import com.facebook.common.logging.FLog
import com.facebook.infer.annotation.Assertions
import com.facebook.react.R
import com.facebook.react.common.ReactConstants
import com.facebook.react.common.annotations.VisibleForTesting
import com.facebook.react.common.annotations.internal.LegacyArchitecture
import com.facebook.react.uimanager.Spacing
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIViewOperationQueue
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.views.text.ReactBaseTextShadowNode
import com.facebook.react.views.text.ReactTextUpdate
import com.facebook.react.views.text.ReactTextViewManagerCallback
import com.facebook.react.views.view.MeasureUtil.getMeasureSpec
import com.facebook.yoga.YogaMeasureFunction
import com.facebook.yoga.YogaMeasureMode
import com.facebook.yoga.YogaMeasureOutput
import com.facebook.yoga.YogaNode
@VisibleForTesting
@LegacyArchitecture
public class ReactTextInputShadowNode
@JvmOverloads
constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? = null) :
ReactBaseTextShadowNode(reactTextViewManagerCallback), YogaMeasureFunction {
private var mostRecentEventCount = ReactConstants.UNSET
private var internalEditText: EditText? = null
private var localData: ReactTextInputLocalData? = null
// Represents the `text` property only, not possible nested content.
@set:ReactProp(name = PROP_TEXT)
public var text: String? = null
set(value) {
field = value
markUpdated()
}
@set:ReactProp(name = PROP_PLACEHOLDER)
public var placeholder: String? = null
set(value) {
field = value
markUpdated()
}
init {
mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY
setMeasureFunction(this)
}
@Suppress("DEPRECATION")
override fun setThemedContext(themedContext: ThemedReactContext) {
super.setThemedContext(themedContext)
// [EditText] has by default a border at the bottom of its view
// called "underline". To have a native look and feel of the TextEdit
// we have to preserve it at least by default.
// The border (underline) has its padding set by the background image
// provided by the system (which vary a lot among versions and vendors
// of Android), and it cannot be changed.
// So, we have to enforce it as a default padding.
// TODO #7120264: Cache this stuff better.
val editText = createInternalEditText()
setDefaultPadding(Spacing.START, ViewCompat.getPaddingStart(editText).toFloat())
setDefaultPadding(Spacing.TOP, editText.paddingTop.toFloat())
setDefaultPadding(Spacing.END, ViewCompat.getPaddingEnd(editText).toFloat())
setDefaultPadding(Spacing.BOTTOM, editText.paddingBottom.toFloat())
internalEditText = editText
// We must measure the EditText without paddings, so we have to reset them.
internalEditText?.setPadding(0, 0, 0, 0)
// This is needed to fix an android bug since 4.4.3 which will throw an NPE in measure,
// setting the layoutParams fixes it: https://code.google.com/p/android/issues/detail?id=75877
internalEditText?.layoutParams =
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
override fun measure(
node: YogaNode,
width: Float,
widthMode: YogaMeasureMode,
height: Float,
heightMode: YogaMeasureMode
): Long {
// measure() should never be called before setThemedContext()
val editText = checkNotNull(internalEditText)
if (localData != null) {
localData?.apply(editText)
} else {
editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextAttributes.effectiveFontSize.toFloat())
if (mNumberOfLines != ReactConstants.UNSET) {
editText.setLines(mNumberOfLines)
}
@SuppressLint("WrongConstant")
if (editText.breakStrategy != mTextBreakStrategy) {
editText.breakStrategy = mTextBreakStrategy
}
}
// make sure the placeholder content is also being measured
editText.hint = placeholder
editText.measure(getMeasureSpec(width, widthMode), getMeasureSpec(height, heightMode))
return YogaMeasureOutput.make(editText.measuredWidth, editText.measuredHeight)
}
override fun isVirtualAnchor(): Boolean = true
override fun isYogaLeafNode(): Boolean = true
override fun setLocalData(data: Any) {
Assertions.assertCondition(data is ReactTextInputLocalData)
localData = data as ReactTextInputLocalData
// Telling to Yoga that the node should be remeasured on next layout pass.
dirty()
// Note: We should NOT mark the node updated (by calling {@code markUpdated}) here
// because the state remains the same.
}
@ReactProp(name = "mostRecentEventCount")
public fun setMostRecentEventCount(mostRecentEventCount: Int) {
this.mostRecentEventCount = mostRecentEventCount
}
override fun setTextBreakStrategy(textBreakStrategy: String?) {
when (textBreakStrategy) {
null,
"simple" -> mTextBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE
"highQuality" -> mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY
"balanced" -> mTextBreakStrategy = Layout.BREAK_STRATEGY_BALANCED
else -> {
FLog.w(ReactConstants.TAG, "Invalid textBreakStrategy: $textBreakStrategy")
mTextBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE
}
}
}
override fun onCollectExtraUpdates(uiViewOperationQueue: UIViewOperationQueue) {
super.onCollectExtraUpdates(uiViewOperationQueue)
if (mostRecentEventCount != ReactConstants.UNSET) {
val reactTextUpdate =
ReactTextUpdate(
spannedFromShadowNode(
this,
text, /* supportsInlineViews: */
false, /* nativeViewHierarchyOptimizer: */
null // only needed to support inline views
),
mostRecentEventCount,
mContainsImages,
getPadding(Spacing.LEFT),
getPadding(Spacing.TOP),
getPadding(Spacing.RIGHT),
getPadding(Spacing.BOTTOM),
mTextAlign,
mTextBreakStrategy,
mJustificationMode)
uiViewOperationQueue.enqueueUpdateExtraData(reactTag, reactTextUpdate)
}
}
override fun setPadding(spacingType: Int, padding: Float) {
super.setPadding(spacingType, padding)
markUpdated()
}
/**
* May be overridden by subclasses that would like to provide their own instance of the internal
* `EditText` this class uses to determine the expected size of the view.
*/
private fun createInternalEditText(): EditText {
// By setting a style which has a background drawable, this EditText will have a different
// background drawable instance from that on the UI Thread, which maybe has a default background
// drawable instance.
// Otherwise, DrawableContainer is not a thread safe class, and it caused the npe in #29452.
val context =
ContextThemeWrapper(themedContext, R.style.Theme_ReactNative_TextInput_DefaultBackground)
return EditText(context)
}
public companion object {
@VisibleForTesting public const val PROP_TEXT: String = "text"
@VisibleForTesting public const val PROP_PLACEHOLDER: String = "placeholder"
}
}