• Underspanding Spans
  • Originally written by Florina Muntenescu
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: tanglie1993
  • Proofread by: Dandyxu, ALVINYEH

Span styles text and paragraphs by letting users use components such as TextPaint and Canvas. In the last article, we discussed how to use a Span, what a Span is, what comes with it, and how to implement and test your own Span.

  • Set a racing boat text style with SpanTo set text styles in Android, use Span! Change the color of some text so that it is clickable and zoomed

Let’s look at what apis can be used to ensure the best performance in a particular use case. We’ll explore the principles behind spans and how the framework uses them. Finally, we’ll look at how spans are passed in and across processes, and based on this, what pitfalls you need to be aware of when creating custom spans.

Principle: How does SPAN work

The Android framework covers text styling and span in several classes: TextView, EditText, Layout classes (Layout, StaticLayout, DynamicLayout), and TextLine (a package private class in Layout) and it depends on several parameters:

  • Text type: selectable, editable or unselectable.
  • BufferType
  • TextViewLayoutParamstype
  • , etc.

The framework checks whether these Spanned objects contain different types of Spans in the framework and triggers the appropriate behavior.

The logic behind text layout and drawing is complex and spread across different classes; In this section, we can only briefly explain how text is processed in a few cases.

Each time a span changes, TextView spanChange checks whether a span is an instance of UpdateAppearance, ParagraphStyle, or CharacterStyle, and, if so, Call the invalidate method on yourself to trigger a view redraw.

The TextLine class represents a line of styled text, and it accepts only subclasses of CharacterStyle, MetricAffectingSpan, and ReplacementSpan. This is triggered MetricAffectingSpan. UpdateMeasureState and CharacterStyle. UpdateDrawState class.

The base class for managing text layouts is Android.text.Layout. Layout and two subclasses, StaticLayout and DynamicLayout, examine the span set to text and calculate line height and Layout margin. In addition, when a span is displayed and updated in DynamicLayout, layout checks if the span is an UpdateLayout and generates a new layout for the affected text.

Ensure best performance when setting up text

There are several ways to save memory when setting the text of a TextView, depending on your needs.

1. For someone who will never changeTextViewSet the text

If you only need to set the text of the TextView once and never need to update it, you can create a new instance of SpannableString or SpannableStringBuilder, Set the desired span and call textView.settext (spannable). Since you are no longer modifying the text, there is no room for performance improvement.

2. Change the text style by adding/removing span

Consider the case where the text itself does not change, but the span attached to it does. For example, when a button is clicked, you want a word in the text to turn gray. So, we need to add a new span to the text. To do this, you will most likely call TextView.settext (CharSequence) twice: the first time to set the initial text and the second time to reset when the button is clicked. A better option is to call TextView.settext (CharSequence, BufferType) and update only the span of the Spannable object when the button is clicked.

Here’s what happens at the bottom in these cases:

Option 1: Call TextView.settext (CharSequence) multiple times – not optimal

When textView.settext (CharSequence) is called, textView secretly copies your Spannable, treats it as a SpannedString, and stores it in memory as a CharSequence. The consequence of this is that your text and span are immutable. So, when you need to update the text style, you will need to create a new Spannable using the text and span, and call TextView.settext again. This will copy the entire object again.

Option 2: Call TextView.settext (CharSequence, BufferType) once and update the Spannable object – best choice

When textView.settext (CharSequence, BufferType) is called, the BufferType argument tells the textView what type of text is set: Static (the default when calling TextView.settext (CharSequence)), styleable/Spannable text, or editable (used by EditText).

Since we are using styled text, we can call:

textView.setText(spannableObject, BufferType.SPANNABLE)
Copy the code

In this case, TextView no longer creates a SpannedString, but it will create a SpannableString with the help of the Spannable.Factory member object. So, TextView now holds a copy of the CharSequence with mutable tags and immutable text.

To update the span, we first get the text as Spannable and then update the span as needed.

/ / ifsetText 被以 BufferType.SPANNABLE 方式调用
textView.setText(spannable, BufferType.SPANNABLE)

// 文字可被转为 Spannable
val spannableText = textView.text as Spannable

// 现在我们可以设置或删除 span
spannableText.setSpan(
     ForegroundColorSpan(color), 
     8, spannableText.length, 
     SPAN_INCLUSIVE_INCLUSIVE)
Copy the code

With this option, we create the initial Spannable object. TextView will hold a copy of it, but when we need to adjust it, we don’t need to create any other objects, because we will be directly manipulating the Spannable text instance that TextView holds. However, TextView will only be notified of span’s add/remove/reorder operations. If you change an internal attribute of a span, you will need to call invalidate() or requestLayout(), depending on the type of change. You can see the details below under additional Performance Suggestions.

3. Text change (reuse TextView)

Suppose we want to reuse TextView and set the text multiple times, just as we did in RecyclerView.viewholder. By default, regardless of BufferType, TextView creates a copy of the CharSequence object and stores it in memory. This ensures that all TextView updates are triggered on purpose and not accidentally when the user changes the value of the CharSequence for some other reason.

In option 2 above, we see that when setting text via TextView.settext (spannableObject, bufferType.spannable), TextView. Spannable. Factory instance to create a new SpannableString, and copy the CharSequence. So every time we set a new text, it creates a new object. If you want more control over the process and avoid extra object creation, implement your own Spannable.Factory, override newSpannable(CharSequence), and set it to the TextView.

In our own implementation, we wanted to avoid creating new objects, so we just returned the CharSequence and turned it into Spannable. Remember, to do this, you need to call textView.settext (spannableObject, BufferType.spannable). Otherwise, the source CharSequence will be an instance of Spanned, and it cannot be turned into a Spannable, causing a ClassCastException.

val spannableFactory = object : Spannable.Factory() {
    override fun newSpannable(source: CharSequence?) : Spannable {return source as Spannable
    }
}
Copy the code

As soon as you get the reference to the TextView, set the spannable.factory object. If you’re using RecyclerView, do this the first time you create your View.

textView.setSpannableFactory(spannableFactory)

This way, you can prevent creating additional objects every time RecyclerView binds a new item to your ViewHolder.

For better performance when using text and RecyclerViews, do not create your Spannable object from the String in ViewHolder, do so before you pass the list to Adapter. This allows you to create a Spannable object in a background thread and do whatever you need to do with the list elements. Your Adapter can hold a reference to List

.

Additional performance recommendations

If you only need to change the internal properties of a span, change its color in the custom emphasis span), you don’t need to call TextView.settext again, just call invalidate() or requestLayout(). Calling setText again will trigger unnecessary business logic and create unnecessary objects when all you need to do is redraw or measure.

All you need to do is hold a reference to the mutable span, and, depending on what properties of the view you change, call:

  • TextView.invalidate()If you just changeText appearance) to trigger onceredrawSkip the layout procedure.
  • TextView.requestLayout()(If you changeText size), then the view can handle itThe measure, layout, and the draw.

If you implement custom weights, the default color is red. When you press a button, you want the color of the weight to turn gray. Your implementation looks like this:

class MainActivity : AppCompatActivity() { // keeping the span as a field val bulletSpan = BulletPointSpan(color = Color.RED) override fun onCreate(savedInstanceState: Bundle?) {... Val spannable = SpannableString(" Text is spantastic ") // Setting the span to the bulletSpan field spannable.setspan ( bulletSpan, 0, 4, Spanned.SPAN_INCLUSIVE_INCLUSIVE) styledText.setText(spannable) button.setOnClickListener( { // change the color of our Color = color.gray // color won't be changed until invalidate is called styledText. Invalidate () }}Copy the code

Bottom layer: Span delivery within and across processes

Too long to look at the version

The custom SPAN feature will not be used for span delivery within and across processes. If the style you want can be implemented using a span that comes with the framework, use as many as possible instead of your own span. Otherwise, try to implement some basic interface or abstract class when you customize a SPAN.

In Android, text can be delivered within processes (or across processes), such as through intents between activities, or across processes when text is delivered between apps.

Custom SPAN implementations cannot be passed between processes because other processes do not know about them or what to do with them. Spans in the Android framework are global objects, but only those that inherit from ParcelableSpan can be passed within or across processes. This feature allows all attributes of a span defined by the framework to implement parcel and unparcel. The textutils. writeToParcel method is responsible for storing span information in a Parcel.

For example, you can pass a span in the same process, or between activities with an intent:

Intent = intent (this, MainActivity::class.java) intent.putextra (TEXT_EXTRA, MySpannableString) startActivity(Intent) // Read the text with Span val intentCharSequence = intent.getCharSequenceExtra(TEXT_EXTRA)Copy the code

So, even if you pass a span in the same process, only ParcelableSpan in the framework survives passing it in an Intent.

ParcelableSpan also allows you to pass text and span across processes. Copy/paste text is implemented via ClipboardService, which uses the same textutil.writetoparcel method at the bottom. So, if you copy/paste a Span inside the same app, this will be a cross-process action and will need to parcel because the text will need to go through ClipboardService.

By default, any class that implements Parcelable can be written to and recovered from a Parcel. When passing Parcelable objects across processes, only the framework classes are guaranteed to be properly accessed. If the data type is defined in different apps, the process trying to recover the data cannot create the object, and the process will crash.

There are two important caveats:

  1. When text with a span is passed, either within or across processes, only the Framework’s ParcelableSpan references are retained. This results in custom SPAN styles not being passed.
  2. You can’t create your own ParcelableSpan.To prevent crashes caused by unknown data types, the framework does not allow customizationParcelableSpan. This is done by puttinggetSpanTypeIdInternalwriteToParcelInternalSet to hide method implementation. They are allTextUtils.writeToParcelUse.

If you need to define a focus span, it can customize the size of the focus because existing BulletSpan has a radius of 4px. Here’s how to do it, and the consequences of each:

  1. Create a BulletSpan that inherits from CustomBulletSpan, which allows you to set the size for the emphasis number. When a span is passed by copying text or jumping between activities, the span attached to the text will be BulletSpan. This means that if text is drawn, it will have the framework’s default text radius, not the radius set in CustomBulletSpan.

  2. Create a CustomBulletSpan that inherits LeadingMarginSpan and re-implement the emphasis function. When a span is passed by copying text or jumping between activities, the span attached to the text will be LeadingMarginSpan. This means that if the text is drawn, it will lose all style.

If the style you want can be implemented using a span that comes with the framework, use as many as possible instead of your own span. Otherwise, try to implement some basic interface or abstract class when you customize a SPAN. This way, you can prevent the implementation of the framework from being applied to spannable during in-process or cross-process passes.


By understanding how Android renders text with span, you’ll hopefully be able to use it effectively in your app. The next time you need to style text, decide whether to use multiple frame spans or implement custom spans depending on how you want to use the text in the future.

Using text in Android is a common operation, and calling the correct TextView.settext method will help you reduce your app’s memory consumption and improve its performance.

Thanks to Siyamed Sinir, Clara Bayarri, Nick Butcher, and Daniel Galpin.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.