Skip to content

Commit 8e4d542

Browse files
NickGerlemanmeta-codesync[bot]
authored andcommitted
Move Linkification for dataDetectorType into TextLayoutManager (fixes dataDetectorType with Facsimile)
Summary: Move `dataDetectorType` Linkify handling from `ReactTextView` (view-level `Linkify` on the `TextView`) to `TextLayoutManager.kt` (`Linkify` on the `Spannable`), so it works for both `ReactTextView` and `PreparedLayoutTextView`. Previously, `dataDetectorType` was handled as a `ReactProp` on `ReactTextViewManager`, which called `Linkify.addLinks()` inside `ReactTextView.setText()`. This meant the feature only worked for the legacy `ReactTextView` path, not for the new `PreparedLayoutTextView`. This diff: - Moves the `DataDetectorType` C++ enum from `components/text/platform/android/` to `attributedstring/primitives.h` (where all other paragraph-related enums live), replacing the original with a forwarding include. - Moves the `DataDetectorType` conversion functions to `attributedstring/conversions.h`, replacing the originals with a forwarding include. - Adds `dataDetectorType` as a field on `ParagraphAttributes` (with hash, equality, and debug support). - Serializes `dataDetectorType` via MapBuffer (key=9) so it flows from C++ to Java. - Adds `REBUILD_FIELD_SWITCH_CASE` for `dataDetectorType` in `BaseParagraphProps.cpp`. - In `TextLayoutManager.kt`, reads `PA_KEY_DATA_DETECTOR_TYPE` from the paragraph attributes MapBuffer and calls `Linkify.addLinks()` on the `Spannable` before layout creation. - Removes the `ReactProp` handler and `setLinkifyMask`/`mLinkifyMaskType` from `ReactTextViewManager.kt` and `ReactTextView.java`. - Adds URLSpan-based `LinkMovementMethod` detection in `ReactTextView.setText()` so that links added by `TextLayoutManager` remain clickable. `HostPlatformParagraphProps` is intentionally left unchanged—`dataDetectorType` is parsed into both `BaseParagraphProps::paragraphAttributes` (for MapBuffer) and `HostPlatformParagraphProps` (for getDiffProps). Changelog: [Internal] Differential Revision: D94017172
1 parent 481134a commit 8e4d542

13 files changed

Lines changed: 172 additions & 125 deletions

File tree

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6056,6 +6056,7 @@ public final class com/facebook/react/views/text/DefaultStyleValuesUtil {
60566056
public static final fun getDefaultTextColor (Landroid/content/Context;)Landroid/content/res/ColorStateList;
60576057
public static final fun getDefaultTextColorHighlight (Landroid/content/Context;)I
60586058
public static final fun getDefaultTextColorHint (Landroid/content/Context;)Landroid/content/res/ColorStateList;
6059+
public static final fun getDefaultTextColorLink (Landroid/content/Context;)I
60596060
public static final fun getTextColorSecondary (Landroid/content/Context;)Landroid/content/res/ColorStateList;
60606061
}
60616062

@@ -6100,7 +6101,6 @@ public class com/facebook/react/views/text/ReactTextView : androidx/appcompat/wi
61006101
public fun setHyphenationFrequency (I)V
61016102
public fun setIncludeFontPadding (Z)V
61026103
public fun setLetterSpacing (F)V
6103-
public fun setLinkifyMask (I)V
61046104
public fun setMinimumFontSize (F)V
61056105
public fun setNumberOfLines (I)V
61066106
public fun setOverflow (Ljava/lang/String;)V
@@ -6135,7 +6135,6 @@ public final class com/facebook/react/views/text/ReactTextViewManager : com/face
61356135
public final fun setBorderRadius (Lcom/facebook/react/views/text/ReactTextView;IF)V
61366136
public final fun setBorderStyle (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V
61376137
public final fun setBorderWidth (Lcom/facebook/react/views/text/ReactTextView;IF)V
6138-
public final fun setDataDetectorType (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V
61396138
public final fun setDisabled (Lcom/facebook/react/views/text/ReactTextView;Z)V
61406139
public final fun setEllipsizeMode (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V
61416140
public final fun setFontSize (Lcom/facebook/react/views/text/ReactTextView;F)V

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/DefaultStyleValuesUtil.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ public object DefaultStyleValuesUtil {
5656
getDefaultTextAttribute(context, android.R.attr.textColorHighlight)?.defaultColor
5757
?: 0 // if the highlight color is not defined in the theme, return black
5858

59+
/**
60+
* Utility method that returns the default text link color as defined by the theme
61+
*
62+
* @param context The Context
63+
* @return The default color int for links as defined in the style
64+
*/
65+
@JvmStatic
66+
public fun getDefaultTextColorLink(context: Context): Int =
67+
getDefaultTextAttribute(context, android.R.attr.textColorLink)?.defaultColor
68+
?: 0xFF0000EE.toInt() // fallback to a standard blue if not defined by the theme
69+
5970
private fun getDefaultTextAttribute(context: Context, attribute: Int): ColorStateList? {
6071
context.theme.obtainStyledAttributes(intArrayOf(attribute)).use { typedArray ->
6172
return typedArray.getColorStateList(0)

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@
1313
import android.os.Build;
1414
import android.text.Layout;
1515
import android.text.Spannable;
16-
import android.text.SpannableString;
1716
import android.text.Spanned;
1817
import android.text.TextUtils;
1918
import android.text.method.LinkMovementMethod;
20-
import android.text.util.Linkify;
19+
import android.text.style.URLSpan;
2120
import android.util.TypedValue;
2221
import android.view.Gravity;
2322
import android.view.KeyEvent;
@@ -71,7 +70,6 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
7170
private float mFontSize;
7271
private float mMinimumFontSize;
7372
private float mLetterSpacing;
74-
private int mLinkifyMaskType;
7573
private boolean mTextIsSelectable;
7674
private boolean mShouldAdjustSpannableFontSize;
7775
private Overflow mOverflow = Overflow.VISIBLE;
@@ -91,7 +89,6 @@ public ReactTextView(Context context) {
9189
private void initView() {
9290
mNumberOfLines = ViewDefaults.NUMBER_OF_LINES;
9391
mAdjustsFontSizeToFit = false;
94-
mLinkifyMaskType = 0;
9592
mTextIsSelectable = false;
9693
mShouldAdjustSpannableFontSize = false;
9794
mEllipsizeLocation = TextUtils.TruncateAt.END;
@@ -131,17 +128,13 @@ private void initView() {
131128
setGravity(DEFAULT_GRAVITY);
132129
setNumberOfLines(mNumberOfLines);
133130
setAdjustFontSizeToFit(mAdjustsFontSizeToFit);
134-
setLinkifyMask(mLinkifyMaskType);
135131
setTextIsSelectable(mTextIsSelectable);
136132

137133
// Default true:
138134
// https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/widget/TextView.java#L9347
139135
setIncludeFontPadding(true);
140136
setEnabled(true);
141137

142-
// reset data detectors
143-
setLinkifyMask(0);
144-
145138
setEllipsizeLocation(mEllipsizeLocation);
146139

147140
// View flags - defaults are here:
@@ -386,14 +379,14 @@ public void setText(ReactTextUpdate update) {
386379
setLayoutParams(EMPTY_LAYOUT_PARAMS);
387380
}
388381
Spanned spanned = update.getText();
389-
if (mLinkifyMaskType > 0) {
390-
if (!(spanned instanceof Spannable)) {
391-
spanned = new SpannableString(spanned);
392-
}
393-
Linkify.addLinks((Spannable) spanned, mLinkifyMaskType);
382+
setText(spanned);
383+
384+
URLSpan[] urlSpans = spanned.getSpans(0, spanned.length(), URLSpan.class);
385+
if (urlSpans != null && urlSpans.length > 0) {
394386
setMovementMethod(LinkMovementMethod.getInstance());
387+
} else if (getMovementMethod() instanceof LinkMovementMethod) {
388+
setMovementMethod(null);
395389
}
396-
setText(spanned);
397390

398391
int nextTextAlign = update.getTextAlign();
399392
if (nextTextAlign != getGravityHorizontal()) {
@@ -627,10 +620,6 @@ public void setSpanned(Spannable spanned) {
627620
return mSpanned;
628621
}
629622

630-
public void setLinkifyMask(int mask) {
631-
mLinkifyMaskType = mask;
632-
}
633-
634623
@Override
635624
protected boolean dispatchHoverEvent(MotionEvent event) {
636625
// if this view has an accessibility delegate set, and that delegate supports virtual view

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import android.text.Layout
1414
import android.text.Spannable
1515
import android.text.Spanned
1616
import android.text.TextUtils
17-
import android.text.util.Linkify
1817
import android.view.Gravity
1918
import com.facebook.common.logging.FLog
2019
import com.facebook.react.R
@@ -152,8 +151,10 @@ public constructor(
152151
TextLayoutManager.getOrCreateSpannableForText(
153152
view.context,
154153
attributedString,
154+
paragraphAttributes,
155155
reactTextViewManagerCallback,
156156
)
157+
157158
view.setSpanned(spanned)
158159

159160
val minimumFontSize: Float =
@@ -345,31 +346,6 @@ public constructor(
345346
view.isEnabled = !disabled
346347
}
347348

348-
@ReactProp(name = "dataDetectorType")
349-
public fun setDataDetectorType(view: ReactTextView, type: String?) {
350-
when (type) {
351-
"phoneNumber" -> {
352-
view.setLinkifyMask(Linkify.PHONE_NUMBERS)
353-
return
354-
}
355-
"link" -> {
356-
view.setLinkifyMask(Linkify.WEB_URLS)
357-
return
358-
}
359-
"email" -> {
360-
view.setLinkifyMask(Linkify.EMAIL_ADDRESSES)
361-
return
362-
}
363-
"all" -> {
364-
@Suppress("DEPRECATION") view.setLinkifyMask(Linkify.ALL)
365-
return
366-
}
367-
}
368-
369-
// "none" case, default, and null type are equivalent.
370-
view.setLinkifyMask(0)
371-
}
372-
373349
public companion object {
374350
private const val TX_STATE_KEY_ATTRIBUTED_STRING: Short = 0
375351
private const val TX_STATE_KEY_PARAGRAPH_ATTRIBUTES: Short = 1

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import android.text.StaticLayout
2121
import android.text.TextDirectionHeuristics
2222
import android.text.TextPaint
2323
import android.text.TextUtils
24+
import android.text.util.Linkify
2425
import android.util.LayoutDirection
2526
import android.view.Gravity
2627
import android.view.View
@@ -88,6 +89,7 @@ internal object TextLayoutManager {
8889
const val PA_KEY_MINIMUM_FONT_SIZE: Int = 6
8990
const val PA_KEY_MAXIMUM_FONT_SIZE: Int = 7
9091
const val PA_KEY_TEXT_ALIGN_VERTICAL: Int = 8
92+
const val PA_KEY_DATA_DETECTOR_TYPE: Int = 9
9193

9294
private val TAG: String = TextLayoutManager::class.java.simpleName
9395

@@ -530,6 +532,7 @@ internal object TextLayoutManager {
530532
fun getOrCreateSpannableForText(
531533
context: Context,
532534
attributedString: MapBuffer,
535+
paragraphAttributes: MapBuffer,
533536
reactTextViewManagerCallback: ReactTextViewManagerCallback?,
534537
): Spannable {
535538
var text: Spannable?
@@ -541,6 +544,7 @@ internal object TextLayoutManager {
541544
createSpannableFromAttributedString(
542545
context,
543546
attributedString.getMapBuffer(AS_KEY_FRAGMENTS),
547+
paragraphAttributes,
544548
reactTextViewManagerCallback,
545549
null,
546550
)
@@ -552,37 +556,47 @@ internal object TextLayoutManager {
552556
private fun createSpannableFromAttributedString(
553557
context: Context,
554558
fragments: MapBuffer,
559+
paragraphAttributes: MapBuffer,
555560
reactTextViewManagerCallback: ReactTextViewManagerCallback?,
556561
outputReactTags: IntArray?,
557562
): Spannable {
558-
if (ReactNativeFeatureFlags.enableAndroidTextMeasurementOptimizations()) {
559-
val spannable = buildSpannableFromFragmentsOptimized(context, fragments, outputReactTags)
563+
val spannable =
564+
if (ReactNativeFeatureFlags.enableAndroidTextMeasurementOptimizations()) {
565+
val s = buildSpannableFromFragmentsOptimized(context, fragments, outputReactTags)
566+
reactTextViewManagerCallback?.onPostProcessSpannable(s)
567+
s
568+
} else {
569+
val sb = SpannableStringBuilder()
560570

561-
reactTextViewManagerCallback?.onPostProcessSpannable(spannable)
562-
return spannable
563-
} else {
564-
val sb = SpannableStringBuilder()
571+
// The [SpannableStringBuilder] implementation require setSpan operation to be called
572+
// up-to-bottom, otherwise all the spannables that are within the region for which one may
573+
// set
574+
// a new spannable will be wiped out
575+
val ops: MutableList<SetSpanOperation> = ArrayList()
565576

566-
// The [SpannableStringBuilder] implementation require setSpan operation to be called
567-
// up-to-bottom, otherwise all the spannables that are within the region for which one may set
568-
// a new spannable will be wiped out
569-
val ops: MutableList<SetSpanOperation> = ArrayList()
577+
buildSpannableFromFragments(context, fragments, sb, ops, outputReactTags)
570578

571-
buildSpannableFromFragments(context, fragments, sb, ops, outputReactTags)
579+
// TODO T31905686: add support for inline Images
580+
// While setting the Spans on the final text, we also check whether any of them are
581+
// images.
582+
for (priorityIndex in ops.indices) {
583+
val op = ops[ops.size - priorityIndex - 1]
572584

573-
// TODO T31905686: add support for inline Images
574-
// While setting the Spans on the final text, we also check whether any of them are images.
575-
for (priorityIndex in ops.indices) {
576-
val op = ops[ops.size - priorityIndex - 1]
585+
// Actual order of calling {@code execute} does NOT matter,
586+
// but the {@code priorityIndex} DOES matter.
587+
op.execute(sb, priorityIndex)
588+
}
577589

578-
// Actual order of calling {@code execute} does NOT matter,
579-
// but the {@code priorityIndex} DOES matter.
580-
op.execute(sb, priorityIndex)
581-
}
590+
reactTextViewManagerCallback?.onPostProcessSpannable(sb)
591+
sb
592+
}
582593

583-
reactTextViewManagerCallback?.onPostProcessSpannable(sb)
584-
return sb
594+
val linkifyMask = getLinkifyMask(paragraphAttributes)
595+
if (linkifyMask > 0) {
596+
Linkify.addLinks(spannable, linkifyMask)
585597
}
598+
599+
return spannable
586600
}
587601

588602
private fun createLayout(
@@ -742,6 +756,8 @@ internal object TextLayoutManager {
742756
baseTextAttributes: TextAttributeProps,
743757
context: Context,
744758
) {
759+
paint.linkColor = DefaultStyleValuesUtil.getDefaultTextColorLink(context)
760+
745761
if (baseTextAttributes.fontSize != ReactConstants.UNSET) {
746762
paint.textSize = baseTextAttributes.fontSize.toFloat()
747763
}
@@ -809,7 +825,13 @@ internal object TextLayoutManager {
809825
heightYogaMeasureMode: YogaMeasureMode,
810826
reactTextViewManagerCallback: ReactTextViewManagerCallback?,
811827
): Layout {
812-
val text = getOrCreateSpannableForText(context, attributedString, reactTextViewManagerCallback)
828+
val text =
829+
getOrCreateSpannableForText(
830+
context,
831+
attributedString,
832+
paragraphAttributes,
833+
reactTextViewManagerCallback,
834+
)
813835

814836
val paint: TextPaint
815837
if (attributedString.contains(AS_KEY_CACHE_ID)) {
@@ -915,6 +937,19 @@ internal object TextLayoutManager {
915937
)
916938
}
917939

940+
private fun getLinkifyMask(paragraphAttributes: MapBuffer): Int {
941+
if (!paragraphAttributes.contains(PA_KEY_DATA_DETECTOR_TYPE)) {
942+
return 0
943+
}
944+
return when (paragraphAttributes.getString(PA_KEY_DATA_DETECTOR_TYPE)) {
945+
"phoneNumber" -> Linkify.PHONE_NUMBERS
946+
"link" -> Linkify.WEB_URLS
947+
"email" -> Linkify.EMAIL_ADDRESSES
948+
"all" -> @Suppress("DEPRECATION") Linkify.ALL
949+
else -> 0
950+
}
951+
}
952+
918953
@JvmStatic
919954
fun createPreparedLayout(
920955
context: Context,
@@ -932,9 +967,11 @@ internal object TextLayoutManager {
932967
createSpannableFromAttributedString(
933968
context,
934969
fragments,
970+
paragraphAttributes,
935971
reactTextViewManagerCallback,
936972
reactTags,
937973
)
974+
938975
val baseTextAttributes =
939976
TextAttributeProps.fromMapBuffer(attributedString.getMapBuffer(AS_KEY_BASE_ATTRIBUTES))
940977
val layout =

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,6 +1014,7 @@ public open class ReactTextInputManager public constructor() :
10141014
TextLayoutManager.getOrCreateSpannableForText(
10151015
view.context,
10161016
attributedString,
1017+
paragraphAttributes,
10171018
reactTextViewManagerCallback,
10181019
)
10191020

packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,17 @@ bool ParagraphAttributes::operator==(const ParagraphAttributes& rhs) const {
2222
adjustsFontSizeToFit,
2323
includeFontPadding,
2424
android_hyphenationFrequency,
25-
textAlignVertical) ==
25+
textAlignVertical,
26+
dataDetectorType) ==
2627
std::tie(
2728
rhs.maximumNumberOfLines,
2829
rhs.ellipsizeMode,
2930
rhs.textBreakStrategy,
3031
rhs.adjustsFontSizeToFit,
3132
rhs.includeFontPadding,
3233
rhs.android_hyphenationFrequency,
33-
rhs.textAlignVertical) &&
34+
rhs.textAlignVertical,
35+
rhs.dataDetectorType) &&
3436
floatEquality(minimumFontSize, rhs.minimumFontSize) &&
3537
floatEquality(maximumFontSize, rhs.maximumFontSize) &&
3638
floatEquality(minimumFontScale, rhs.minimumFontScale);
@@ -75,7 +77,11 @@ SharedDebugStringConvertibleList ParagraphAttributes::getDebugProps() const {
7577
debugStringConvertibleItem(
7678
"textAlignVertical",
7779
textAlignVertical,
78-
paragraphAttributes.textAlignVertical)};
80+
paragraphAttributes.textAlignVertical),
81+
debugStringConvertibleItem(
82+
"dataDetectorType",
83+
dataDetectorType,
84+
paragraphAttributes.dataDetectorType)};
7985
}
8086
#endif
8187

packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ class ParagraphAttributes : public DebugStringConvertible {
8282
*/
8383
std::optional<TextAlignmentVertical> textAlignVertical{};
8484

85+
/*
86+
* (Android only) Defines the type of data to be detected and converted to
87+
* clickable URLs in the text.
88+
*/
89+
std::optional<DataDetectorType> dataDetectorType{};
90+
8591
bool operator==(const ParagraphAttributes &rhs) const;
8692

8793
#pragma mark - DebugStringConvertible
@@ -109,7 +115,8 @@ struct hash<facebook::react::ParagraphAttributes> {
109115
attributes.includeFontPadding,
110116
attributes.android_hyphenationFrequency,
111117
attributes.minimumFontScale,
112-
attributes.textAlignVertical);
118+
attributes.textAlignVertical,
119+
attributes.dataDetectorType);
113120
}
114121
};
115122
} // namespace std

0 commit comments

Comments
 (0)