@@ -14,6 +14,7 @@ import android.os.Build
1414import android.text.BoringLayout
1515import android.text.Layout
1616import android.text.Spannable
17+ import android.text.SpannableString
1718import android.text.SpannableStringBuilder
1819import android.text.Spanned
1920import android.text.StaticLayout
@@ -29,6 +30,7 @@ import com.facebook.react.bridge.WritableArray
2930import com.facebook.react.common.ReactConstants
3031import com.facebook.react.common.mapbuffer.MapBuffer
3132import com.facebook.react.common.mapbuffer.ReadableMapBuffer
33+ import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
3234import com.facebook.react.uimanager.PixelUtil
3335import com.facebook.react.uimanager.PixelUtil.dpToPx
3436import com.facebook.react.uimanager.PixelUtil.pxToDp
@@ -310,6 +312,156 @@ internal object TextLayoutManager {
310312 }
311313 }
312314
315+ private class FragmentAttributes (
316+ val props : TextAttributeProps ,
317+ val length : Int ,
318+ val reactTag : Int ,
319+ val isAttachment : Boolean ,
320+ val width : Double ,
321+ val height : Double
322+ )
323+
324+ private fun buildSpannableFromFragmentsOptimized (
325+ context : Context ,
326+ fragments : MapBuffer
327+ ): Spannable {
328+ val text = StringBuilder ()
329+ val parsedFragments = ArrayList <FragmentAttributes >(fragments.count)
330+
331+ for (i in 0 until fragments.count) {
332+ val fragment = fragments.getMapBuffer(i)
333+ val props = TextAttributeProps .fromMapBuffer(fragment.getMapBuffer(FR_KEY_TEXT_ATTRIBUTES ))
334+ val fragmentText = TextTransform .apply (fragment.getString(FR_KEY_STRING ), props.textTransform)
335+ text.append(fragmentText)
336+ parsedFragments.add(
337+ FragmentAttributes (
338+ props = props,
339+ length = fragmentText.length,
340+ reactTag =
341+ if (fragment.contains(FR_KEY_REACT_TAG )) {
342+ fragment.getInt(FR_KEY_REACT_TAG )
343+ } else {
344+ View .NO_ID
345+ },
346+ isAttachment =
347+ fragment.contains(FR_KEY_IS_ATTACHMENT ) &&
348+ fragment.getBoolean(FR_KEY_IS_ATTACHMENT ),
349+ width =
350+ if (fragment.contains(FR_KEY_WIDTH )) {
351+ fragment.getDouble(FR_KEY_WIDTH )
352+ } else {
353+ Double .NaN
354+ },
355+ height =
356+ if (fragment.contains(FR_KEY_HEIGHT )) {
357+ fragment.getDouble(FR_KEY_HEIGHT )
358+ } else {
359+ Double .NaN
360+ }))
361+ }
362+
363+ val spannable = SpannableString (text)
364+
365+ var start = 0
366+ for (fragment in parsedFragments) {
367+ val end = start + fragment.length
368+ val spanFlags =
369+ if (start == 0 ) Spannable .SPAN_INCLUSIVE_INCLUSIVE else Spannable .SPAN_EXCLUSIVE_INCLUSIVE
370+
371+ if (fragment.isAttachment) {
372+ spannable.setSpan(
373+ TextInlineViewPlaceholderSpan (
374+ fragment.reactTag,
375+ PixelUtil .toPixelFromSP(fragment.width).toInt(),
376+ PixelUtil .toPixelFromSP(fragment.height).toInt()),
377+ start,
378+ end,
379+ spanFlags)
380+ } else {
381+ val roleIsLink =
382+ if (fragment.props.role != null )
383+ (fragment.props.role == ReactAccessibilityDelegate .Role .LINK )
384+ else
385+ (fragment.props.accessibilityRole ==
386+ ReactAccessibilityDelegate .AccessibilityRole .LINK )
387+
388+ if (roleIsLink) {
389+ spannable.setSpan(ReactClickableSpan (fragment.reactTag), start, end, spanFlags)
390+ }
391+
392+ if (fragment.props.isColorSet) {
393+ spannable.setSpan(ReactForegroundColorSpan (fragment.props.color), start, end, spanFlags)
394+ }
395+
396+ if (fragment.props.isBackgroundColorSet) {
397+ spannable.setSpan(
398+ ReactBackgroundColorSpan (fragment.props.backgroundColor), start, end, spanFlags)
399+ }
400+
401+ if (! fragment.props.opacity.isNaN()) {
402+ spannable.setSpan(ReactOpacitySpan (fragment.props.opacity), start, end, spanFlags)
403+ }
404+
405+ if (! fragment.props.letterSpacing.isNaN()) {
406+ spannable.setSpan(
407+ CustomLetterSpacingSpan (fragment.props.letterSpacing), start, end, spanFlags)
408+ }
409+
410+ // TODO: Should this be using effectiveFontSize instead of fontSize?
411+ spannable.setSpan(ReactAbsoluteSizeSpan (fragment.props.mFontSize), start, end, spanFlags)
412+
413+ if (fragment.props.fontStyle != ReactConstants .UNSET ||
414+ fragment.props.fontWeight != ReactConstants .UNSET ||
415+ fragment.props.fontFamily != null ) {
416+ spannable.setSpan(
417+ CustomStyleSpan (
418+ fragment.props.fontStyle,
419+ fragment.props.fontWeight,
420+ fragment.props.fontFeatureSettings,
421+ fragment.props.fontFamily,
422+ context.assets),
423+ start,
424+ end,
425+ spanFlags)
426+ }
427+
428+ if (fragment.props.isUnderlineTextDecorationSet) {
429+ spannable.setSpan(ReactUnderlineSpan (), start, end, spanFlags)
430+ }
431+
432+ if (fragment.props.isLineThroughTextDecorationSet) {
433+ spannable.setSpan(ReactStrikethroughSpan (), start, end, spanFlags)
434+ }
435+
436+ if ((fragment.props.textShadowOffsetDx != 0f ||
437+ fragment.props.textShadowOffsetDy != 0f ||
438+ fragment.props.textShadowRadius != 0f ) &&
439+ Color .alpha(fragment.props.textShadowColor) != 0 ) {
440+ spannable.setSpan(
441+ ShadowStyleSpan (
442+ fragment.props.textShadowOffsetDx,
443+ fragment.props.textShadowOffsetDy,
444+ fragment.props.textShadowRadius,
445+ fragment.props.textShadowColor),
446+ start,
447+ end,
448+ spanFlags)
449+ }
450+
451+ if (! fragment.props.effectiveLineHeight.isNaN()) {
452+ spannable.setSpan(
453+ CustomLineHeightSpan (fragment.props.effectiveLineHeight), start, end, spanFlags)
454+ }
455+
456+ spannable.setSpan(ReactTagSpan (fragment.reactTag), start, end, spanFlags)
457+ }
458+
459+ start = end
460+ }
461+
462+ return spannable
463+ }
464+
313465 fun getOrCreateSpannableForText (
314466 context : Context ,
315467 attributedString : MapBuffer ,
@@ -333,27 +485,36 @@ internal object TextLayoutManager {
333485 attributedString : MapBuffer ,
334486 reactTextViewManagerCallback : ReactTextViewManagerCallback ?
335487 ): Spannable {
336- val sb = SpannableStringBuilder ()
488+ if (ReactNativeFeatureFlags .enableAndroidTextMeasurementOptimizations()) {
489+ val spannable =
490+ buildSpannableFromFragmentsOptimized(
491+ context, attributedString.getMapBuffer(AS_KEY_FRAGMENTS ))
337492
338- // The [SpannableStringBuilder] implementation require setSpan operation to be called
339- // up-to-bottom, otherwise all the spannables that are within the region for which one may set
340- // a new spannable will be wiped out
341- val ops : MutableList < SetSpanOperation > = ArrayList ()
493+ reactTextViewManagerCallback?.onPostProcessSpannable(spannable)
494+ return spannable
495+ } else {
496+ val sb = SpannableStringBuilder ()
342497
343- buildSpannableFromFragments(context, attributedString.getMapBuffer(AS_KEY_FRAGMENTS ), sb, ops)
498+ // The [SpannableStringBuilder] implementation require setSpan operation to be called
499+ // up-to-bottom, otherwise all the spannables that are within the region for which one may set
500+ // a new spannable will be wiped out
501+ val ops: MutableList <SetSpanOperation > = ArrayList ()
344502
345- // TODO T31905686: add support for inline Images
346- // While setting the Spans on the final text, we also check whether any of them are images.
347- for (priorityIndex in ops.indices) {
348- val op = ops[ops.size - priorityIndex - 1 ]
503+ buildSpannableFromFragments(context, attributedString.getMapBuffer(AS_KEY_FRAGMENTS ), sb, ops)
349504
350- // Actual order of calling {@code execute} does NOT matter,
351- // but the {@code priorityIndex} DOES matter .
352- op.execute(sb, priorityIndex)
353- }
505+ // TODO T31905686: add support for inline Images
506+ // While setting the Spans on the final text, we also check whether any of them are images .
507+ for ( priorityIndex in ops.indices) {
508+ val op = ops[ops.size - priorityIndex - 1 ]
354509
355- reactTextViewManagerCallback?.onPostProcessSpannable(sb)
356- return sb
510+ // Actual order of calling {@code execute} does NOT matter,
511+ // but the {@code priorityIndex} DOES matter.
512+ op.execute(sb, priorityIndex)
513+ }
514+
515+ reactTextViewManagerCallback?.onPostProcessSpannable(sb)
516+ return sb
517+ }
357518 }
358519
359520 private fun createLayout (
0 commit comments