Skip to content

Commit 0d455f3

Browse files
NickGerlemanfacebook-github-bot
authored andcommitted
buildSpannableFromFragmentsOptimized (#52385)
Summary: Pull Request resolved: #52385 This replaces `buildSpannableFromFragmentsOptimized()` with a more optimized version. There are a couple main changes. 1. We don't need a complicated structure around ordering, and span priority, that made its way from the Java ShadowNode logic. AttributedString already ensures there are no overlapping attributes per fragment. 2. `SpannableStringBuilder` is a complicated text-editor style data structure, optimized to allow text content to be modified, and spans re-applied. We can use a much lighter `SpannableString`, on top of the ahead-of-time known text content, which is faster, and saves around 500 bytes per string (and prepared layout). If we assign this to an `EditText`, which later gets edited, Android will copy it to a `SpannableStringBuilder`. Changelog: [Internal] Reviewed By: lenaic Differential Revision: D77622848 fbshipit-source-id: 69bbac86e1f0fd4a15dab6bc279cca305f2a53ae
1 parent a4b0d64 commit 0d455f3

1 file changed

Lines changed: 177 additions & 16 deletions

File tree

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

Lines changed: 177 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import android.os.Build
1414
import android.text.BoringLayout
1515
import android.text.Layout
1616
import android.text.Spannable
17+
import android.text.SpannableString
1718
import android.text.SpannableStringBuilder
1819
import android.text.Spanned
1920
import android.text.StaticLayout
@@ -29,6 +30,7 @@ import com.facebook.react.bridge.WritableArray
2930
import com.facebook.react.common.ReactConstants
3031
import com.facebook.react.common.mapbuffer.MapBuffer
3132
import com.facebook.react.common.mapbuffer.ReadableMapBuffer
33+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
3234
import com.facebook.react.uimanager.PixelUtil
3335
import com.facebook.react.uimanager.PixelUtil.dpToPx
3436
import 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

Comments
 (0)