1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.core.text;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
20 
21 import android.annotation.SuppressLint;
22 import android.os.Build;
23 import android.os.Trace;
24 import android.text.Layout;
25 import android.text.PrecomputedText;
26 import android.text.Spannable;
27 import android.text.SpannableString;
28 import android.text.StaticLayout;
29 import android.text.TextDirectionHeuristic;
30 import android.text.TextDirectionHeuristics;
31 import android.text.TextPaint;
32 import android.text.TextUtils;
33 import android.text.style.MetricAffectingSpan;
34 
35 import androidx.annotation.GuardedBy;
36 import androidx.annotation.IntRange;
37 import androidx.annotation.RequiresApi;
38 import androidx.annotation.RestrictTo;
39 import androidx.annotation.UiThread;
40 import androidx.core.util.ObjectsCompat;
41 import androidx.core.util.Preconditions;
42 
43 import org.jspecify.annotations.NonNull;
44 import org.jspecify.annotations.Nullable;
45 
46 import java.util.ArrayList;
47 import java.util.concurrent.Callable;
48 import java.util.concurrent.Executor;
49 import java.util.concurrent.Executors;
50 import java.util.concurrent.Future;
51 import java.util.concurrent.FutureTask;
52 
53 /**
54  * A text which has the character metrics data.
55  *
56  * A text object that contains the character metrics data and can be used to improve the performance
57  * of text layout operations. When a PrecomputedTextCompat is created with a given
58  * {@link CharSequence}, it will measure the text metrics during the creation. This PrecomputedText
59  * instance can be set on {@link android.widget.TextView} or {@link StaticLayout}. Since the text
60  * layout information will be included in this instance, {@link android.widget.TextView} or
61  * {@link StaticLayout} will not have to recalculate this information.
62  *
63  * On API 29 or later, there is full PrecomputedText support by framework. From API 21 to API 27,
64  * PrecomputedTextCompat relies on internal text layout cache. PrecomputedTextCompat immediately
65  * computes the text layout in the constuctor to warm up the internal text layout cache. On API 20
66  * or before, PrecomputedTextCompat does nothing.
67  *
68  * Note that any {@link android.text.NoCopySpan} attached to the original text won't be passed to
69  * PrecomputedText.
70  */
71 public class PrecomputedTextCompat implements Spannable {
72     private static final char LINE_FEED = '\n';
73 
74     private static final Object sLock = new Object();
75     @GuardedBy("sLock") private static @NonNull Executor sExecutor = null;
76 
77     /**
78      * The information required for building {@link PrecomputedTextCompat}.
79      *
80      * Contains information required for precomputing text measurement metadata, so it can be done
81      * in isolation of a {@link android.widget.TextView} or {@link StaticLayout}, when final layout
82      * constraints are not known.
83      */
84     public static final class Params {
85         private final @NonNull TextPaint mPaint;
86 
87         // null on API 17 or before, non null on API 18 or later.
88         private final @Nullable TextDirectionHeuristic mTextDir;
89 
90         private final int mBreakStrategy;
91 
92         private final int mHyphenationFrequency;
93 
94         final PrecomputedText.Params mWrapped;
95 
96         /**
97          * A builder for creating {@link Params}.
98          */
99         public static class Builder {
100             // The TextPaint used for measurement.
101             private final @NonNull TextPaint mPaint;
102 
103             // The requested text direction.
104             private TextDirectionHeuristic mTextDir;
105 
106             // The break strategy for this measured text.
107             private int mBreakStrategy;
108 
109             // The hyphenation frequency for this measured text.
110             private int mHyphenationFrequency;
111 
112             /**
113              * Builder constructor.
114              *
115              * @param paint the paint to be used for drawing
116              */
Builder(@onNull TextPaint paint)117             public Builder(@NonNull TextPaint paint) {
118                 mPaint = paint;
119                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
120                     mBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
121                     mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NORMAL;
122                 } else {
123                     mBreakStrategy = mHyphenationFrequency = 0;
124                 }
125                 mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
126             }
127 
128             /**
129              * Set the line break strategy.
130              *
131              * The default value is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}.
132              *
133              * On API 22 and below, this has no effect as there is no line break strategy.
134              *
135              * @param strategy the break strategy
136              * @return PrecomputedTextCompat.Builder instance
137              * @see StaticLayout.Builder#setBreakStrategy
138              * @see android.widget.TextView#setBreakStrategy
139              */
140             @RequiresApi(23)
setBreakStrategy(int strategy)141             public Builder setBreakStrategy(int strategy) {
142                 mBreakStrategy = strategy;
143                 return this;
144             }
145 
146             /**
147              * Set the hyphenation frequency.
148              *
149              * The default value is {@link Layout#HYPHENATION_FREQUENCY_NORMAL}.
150              *
151              * On API 22 and below, this has no effect as there is no hyphenation frequency.
152              *
153              * @param frequency the hyphenation frequency
154              * @return PrecomputedTextCompat.Builder instance
155              * @see StaticLayout.Builder#setHyphenationFrequency
156              * @see android.widget.TextView#setHyphenationFrequency
157              */
158             @RequiresApi(23)
setHyphenationFrequency(int frequency)159             public Builder setHyphenationFrequency(int frequency) {
160                 mHyphenationFrequency = frequency;
161                 return this;
162             }
163 
164             /**
165              * Set the text direction heuristic.
166              *
167              * The default value is {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}.
168              *
169              * On API 17 or before, text direction heuristics cannot be modified, so this method
170              * does nothing.
171              *
172              * @param textDir the text direction heuristic for resolving bidi behavior
173              * @return PrecomputedTextCompat.Builder instance
174              * @see StaticLayout.Builder#setTextDirection
175              */
setTextDirection(@onNull TextDirectionHeuristic textDir)176             public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) {
177                 mTextDir = textDir;
178                 return this;
179             }
180 
181             /**
182              * Build the {@link Params}.
183              *
184              * @return the layout parameter
185              */
build()186             public @NonNull Params build() {
187                 return new Params(mPaint, mTextDir, mBreakStrategy, mHyphenationFrequency);
188             }
189         }
190 
Params(@onNull TextPaint paint, @NonNull TextDirectionHeuristic textDir, int strategy, int frequency)191         Params(@NonNull TextPaint paint, @NonNull TextDirectionHeuristic textDir,
192                 int strategy, int frequency) {
193             if (Build.VERSION.SDK_INT >= 29) {
194                 mWrapped = new PrecomputedText.Params.Builder(paint)
195                         .setBreakStrategy(strategy)
196                         .setHyphenationFrequency(frequency)
197                         .setTextDirection(textDir)
198                         .build();
199             } else {
200                 mWrapped = null;
201             }
202             mPaint = paint;
203             mTextDir = textDir;
204             mBreakStrategy = strategy;
205             mHyphenationFrequency = frequency;
206         }
207 
208         @RequiresApi(28)
Params(PrecomputedText.@onNull Params wrapped)209         public Params(PrecomputedText.@NonNull Params wrapped) {
210             mPaint = wrapped.getTextPaint();
211             mTextDir = wrapped.getTextDirection();
212             mBreakStrategy = wrapped.getBreakStrategy();
213             mHyphenationFrequency = wrapped.getHyphenationFrequency();
214             mWrapped = (Build.VERSION.SDK_INT >= 29) ? wrapped : null;
215         }
216 
217         /**
218          * Returns the {@link TextPaint} for this text.
219          *
220          * @return A {@link TextPaint}
221          */
getTextPaint()222         public @NonNull TextPaint getTextPaint() {
223             return mPaint;
224         }
225 
226         /**
227          * Returns the {@link TextDirectionHeuristic} for this text.
228          *
229          * On API 17 and below, this returns null, otherwise returns non-null
230          * TextDirectionHeuristic.
231          *
232          * @return the {@link TextDirectionHeuristic}
233          */
getTextDirection()234         public @Nullable TextDirectionHeuristic getTextDirection() {
235             return mTextDir;
236         }
237 
238         /**
239          * Returns the break strategy for this text.
240          *
241          * On API 22 and below, this returns 0.
242          *
243          * @return the line break strategy
244          */
245         @RequiresApi(23)
getBreakStrategy()246         public int getBreakStrategy() {
247             return mBreakStrategy;
248         }
249 
250         /**
251          * Returns the hyphenation frequency for this text.
252          *
253          * On API 22 and below, this returns 0.
254          *
255          * @return the hyphenation frequency
256          */
257         @RequiresApi(23)
getHyphenationFrequency()258         public int getHyphenationFrequency() {
259             return mHyphenationFrequency;
260         }
261 
262 
263         /**
264          * Similar to equals but don't compare text direction
265          */
266         @RestrictTo(LIBRARY_GROUP_PREFIX)
equalsWithoutTextDirection(@onNull Params other)267         public boolean equalsWithoutTextDirection(@NonNull Params other) {
268             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
269                 if (mBreakStrategy != other.getBreakStrategy()) {
270                     return false;
271                 }
272                 if (mHyphenationFrequency != other.getHyphenationFrequency()) {
273                     return false;
274                 }
275             }
276 
277             if (mPaint.getTextSize() != other.getTextPaint().getTextSize()) {
278                 return false;
279             }
280             if (mPaint.getTextScaleX() != other.getTextPaint().getTextScaleX()) {
281                 return false;
282             }
283             if (mPaint.getTextSkewX() != other.getTextPaint().getTextSkewX()) {
284                 return false;
285             }
286             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
287                 if (mPaint.getLetterSpacing() != other.getTextPaint().getLetterSpacing()) {
288                     return false;
289                 }
290                 if (!TextUtils.equals(mPaint.getFontFeatureSettings(),
291                         other.getTextPaint().getFontFeatureSettings())) {
292                     return false;
293                 }
294             }
295             if (mPaint.getFlags() != other.getTextPaint().getFlags()) {
296                 return false;
297             }
298             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
299                 if (!mPaint.getTextLocales().equals(other.getTextPaint().getTextLocales())) {
300                     return false;
301                 }
302             } else {
303                 if (!mPaint.getTextLocale().equals(other.getTextPaint().getTextLocale())) {
304                     return false;
305                 }
306             }
307             if (mPaint.getTypeface() == null) {
308                 if (other.getTextPaint().getTypeface() != null) {
309                     return false;
310                 }
311             } else if (!mPaint.getTypeface().equals(other.getTextPaint().getTypeface())) {
312                 return false;
313             }
314 
315             return true;
316         }
317 
318         /**
319          * Check if the same text layout.
320          *
321          * @return true if this and the given param result in the same text layout
322          */
323         @Override
equals(@ullable Object o)324         public boolean equals(@Nullable Object o) {
325             if (o == this) {
326                 return true;
327             }
328             if (!(o instanceof Params)) {
329                 return false;
330             }
331             Params other = (Params) o;
332             if (!equalsWithoutTextDirection(other)) {
333                 return false;
334             }
335             return mTextDir == other.getTextDirection();
336         }
337 
338         @Override
hashCode()339         public int hashCode() {
340             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
341                 return ObjectsCompat.hash(mPaint.getTextSize(), mPaint.getTextScaleX(),
342                         mPaint.getTextSkewX(), mPaint.getLetterSpacing(), mPaint.getFlags(),
343                         mPaint.getTextLocales(), mPaint.getTypeface(), mPaint.isElegantTextHeight(),
344                         mTextDir, mBreakStrategy, mHyphenationFrequency);
345             } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
346                 return ObjectsCompat.hash(mPaint.getTextSize(), mPaint.getTextScaleX(),
347                         mPaint.getTextSkewX(), mPaint.getLetterSpacing(), mPaint.getFlags(),
348                         mPaint.getTextLocale(), mPaint.getTypeface(), mPaint.isElegantTextHeight(),
349                         mTextDir, mBreakStrategy, mHyphenationFrequency);
350             } else {
351                 return ObjectsCompat.hash(mPaint.getTextSize(), mPaint.getTextScaleX(),
352                         mPaint.getTextSkewX(), mPaint.getFlags(), mPaint.getTextLocale(),
353                         mPaint.getTypeface(), mTextDir, mBreakStrategy, mHyphenationFrequency);
354             }
355         }
356 
357         @Override
toString()358         public String toString() {
359             StringBuilder sb = new StringBuilder("{");
360             sb.append("textSize=" + mPaint.getTextSize());
361             sb.append(", textScaleX=" + mPaint.getTextScaleX());
362             sb.append(", textSkewX=" + mPaint.getTextSkewX());
363             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
364                 sb.append(", letterSpacing=" + mPaint.getLetterSpacing());
365                 sb.append(", elegantTextHeight=" + mPaint.isElegantTextHeight());
366             }
367             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
368                 sb.append(", textLocale=" + mPaint.getTextLocales());
369             } else {
370                 sb.append(", textLocale=" + mPaint.getTextLocale());
371             }
372             sb.append(", typeface=" + mPaint.getTypeface());
373             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
374                 sb.append(", variationSettings=" + mPaint.getFontVariationSettings());
375             }
376             sb.append(", textDir=" + mTextDir);
377             sb.append(", breakStrategy=" + mBreakStrategy);
378             sb.append(", hyphenationFrequency=" + mHyphenationFrequency);
379             sb.append("}");
380             return sb.toString();
381         }
382     };
383 
384     // The original text.
385     private final @NonNull Spannable mText;
386 
387     private final @NonNull Params mParams;
388 
389     // The list of measured paragraph info.
390     private final int @NonNull [] mParagraphEnds;
391 
392     // null on API 27 or before. Non-null on API 29 or later
393     private final @Nullable PrecomputedText mWrapped;
394 
395     /**
396      * Create a new {@link PrecomputedText} which will pre-compute text measurement and glyph
397      * positioning information.
398      * <p>
399      * This can be expensive, so computing this on a background thread before your text will be
400      * presented can save work on the UI thread.
401      * </p>
402      *
403      * Note that any {@link android.text.NoCopySpan} attached to the text won't be passed to the
404      * created PrecomputedText.
405      *
406      * @param text the text to be measured
407      * @param params parameters that define how text will be precomputed
408      * @return A {@link PrecomputedText}
409      */
410     @SuppressLint("WrongConstant")
create(@onNull CharSequence text, @NonNull Params params)411     public static PrecomputedTextCompat create(@NonNull CharSequence text, @NonNull Params params) {
412         Preconditions.checkNotNull(text);
413         Preconditions.checkNotNull(params);
414 
415         try {
416             Trace.beginSection("PrecomputedText");
417 
418             if (Build.VERSION.SDK_INT >= 29 && params.mWrapped != null) {
419                 return new PrecomputedTextCompat(
420                         PrecomputedText.create(text, params.mWrapped), params);
421             }
422 
423             ArrayList<Integer> ends = new ArrayList<>();
424 
425             int paraEnd = 0;
426             int end = text.length();
427             for (int paraStart = 0; paraStart < end; paraStart = paraEnd) {
428                 paraEnd = TextUtils.indexOf(text, LINE_FEED, paraStart, end);
429                 if (paraEnd < 0) {
430                     // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph
431                     // end.
432                     paraEnd = end;
433                 } else {
434                     paraEnd++;  // Includes LINE_FEED(U+000A) to the prev paragraph.
435                 }
436 
437                 ends.add(paraEnd);
438             }
439             int[] result = new int[ends.size()];
440             for (int i = 0; i < ends.size(); ++i) {
441                 result[i] = ends.get(i);
442             }
443 
444             // No framework support for PrecomputedText
445             // Compute text layout and throw away StaticLayout for the purpose of warming up the
446             // internal text layout cache.
447             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
448                 StaticLayout.Builder.obtain(text, 0, text.length(), params.getTextPaint(),
449                         Integer.MAX_VALUE)
450                         .setBreakStrategy(params.getBreakStrategy())
451                         .setHyphenationFrequency(params.getHyphenationFrequency())
452                         .setTextDirection(params.getTextDirection())
453                         .build();
454             } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
455                 new StaticLayout(text, params.getTextPaint(), Integer.MAX_VALUE,
456                         Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
457             } else {
458                 // There is no way of precomputing text layout on API 20 or before
459                 // Do nothing
460             }
461 
462             return new PrecomputedTextCompat(text, params, result);
463         } finally {
464             Trace.endSection();
465         }
466     }
467 
468     // Use PrecomputedText.create instead.
PrecomputedTextCompat(@onNull CharSequence text, @NonNull Params params, int @NonNull [] paraEnds)469     private PrecomputedTextCompat(@NonNull CharSequence text, @NonNull Params params,
470             int @NonNull [] paraEnds) {
471         mText = new SpannableString(text);
472         mParams = params;
473         mParagraphEnds = paraEnds;
474         mWrapped = null;
475     }
476 
477     @RequiresApi(28)
PrecomputedTextCompat(@onNull PrecomputedText precomputed, @NonNull Params params)478     private PrecomputedTextCompat(@NonNull PrecomputedText precomputed, @NonNull Params params) {
479         mText = Api28Impl.castToSpannable(precomputed);
480         mParams = params;
481         mParagraphEnds = null;
482         mWrapped = (Build.VERSION.SDK_INT >= 29) ? precomputed : null;
483     }
484 
485     /**
486      * Returns the underlying original text if the text is PrecomputedText.
487      */
488     @RestrictTo(LIBRARY_GROUP_PREFIX)
489     @RequiresApi(28)
getPrecomputedText()490     public @Nullable PrecomputedText getPrecomputedText() {
491         if (mText instanceof PrecomputedText) {
492             return (PrecomputedText) mText;
493         } else {
494             return null;
495         }
496     }
497 
498     /**
499      * Returns the parameters used to measure this text.
500      */
getParams()501     public @NonNull Params getParams() {
502         return mParams;
503     }
504 
505     /**
506      * Returns the count of paragraphs.
507      */
getParagraphCount()508     public @IntRange(from = 0) int getParagraphCount() {
509         if (Build.VERSION.SDK_INT >= 29) {
510             return mWrapped.getParagraphCount();
511         } else {
512             return mParagraphEnds.length;
513         }
514     }
515 
516     /**
517      * Returns the paragraph start offset of the text.
518      */
getParagraphStart(@ntRangefrom = 0) int paraIndex)519     public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) {
520         Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
521         if (Build.VERSION.SDK_INT >= 29) {
522             return mWrapped.getParagraphStart(paraIndex);
523         } else {
524             return paraIndex == 0 ? 0 : mParagraphEnds[paraIndex - 1];
525         }
526     }
527 
528     /**
529      * Returns the paragraph end offset of the text.
530      */
getParagraphEnd(@ntRangefrom = 0) int paraIndex)531     public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) {
532         Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
533         if (Build.VERSION.SDK_INT >= 29) {
534             return mWrapped.getParagraphEnd(paraIndex);
535         } else {
536             return mParagraphEnds[paraIndex];
537         }
538     }
539 
540     /**
541      * A helper class for computing text layout in background
542      */
543     private static class PrecomputedTextFutureTask extends FutureTask<PrecomputedTextCompat> {
544         private static class PrecomputedTextCallback implements Callable<PrecomputedTextCompat> {
545             private PrecomputedTextCompat.Params mParams;
546             private CharSequence mText;
547 
PrecomputedTextCallback(final PrecomputedTextCompat.@NonNull Params params, final @NonNull CharSequence cs)548             PrecomputedTextCallback(final PrecomputedTextCompat.@NonNull Params params,
549                     final @NonNull CharSequence cs) {
550                 mParams = params;
551                 mText = cs;
552             }
553 
554             @Override
call()555             public PrecomputedTextCompat call() throws Exception {
556                 return PrecomputedTextCompat.create(mText, mParams);
557             }
558         }
559 
PrecomputedTextFutureTask(final PrecomputedTextCompat.@NonNull Params params, final @NonNull CharSequence text)560         PrecomputedTextFutureTask(final PrecomputedTextCompat.@NonNull Params params,
561                 final @NonNull CharSequence text) {
562             super(new PrecomputedTextCallback(params, text));
563         }
564     }
565 
566     /**
567      * Helper for PrecomputedText that returns a future to be used with
568      * {@link androidx.appcompat.widget.AppCompatTextView#setTextFuture}.
569      *
570      * PrecomputedText is suited to compute on a background thread, but when TextView properties are
571      * dynamic, it's common to configure text properties and text at the same time, when binding a
572      * View. For example, in a RecyclerView Adapter:
573      * <pre>
574      *     void onBindViewHolder(ViewHolder vh, int position) {
575      *         ItemData data = getData(position);
576      *
577      *         vh.textView.setTextSize(...);
578      *         vh.textView.setFontVariationSettings(...);
579      *         vh.textView.setText(data.text);
580      *     }
581      * </pre>
582      * In such cases, using PrecomputedText is difficult, since it isn't safe to defer the setText()
583      * code arbitrarily - a layout pass may happen before computation finishes, and will be
584      * incorrect if the text isn't ready yet.
585      * <p>
586      * With {@code getTextFuture()}, you can block on the result of the precomputation safely
587      * before the result is needed. AppCompatTextView provides
588      * {@link androidx.appcompat.widget.AppCompatTextView#setTextFuture} for exactly this
589      * use case. With the following code, the app's layout work is largely done on a background
590      * thread:
591      * <pre>
592      *     void onBindViewHolder(ViewHolder vh, int position) {
593      *         ItemData data = getData(position);
594      *
595      *         vh.textView.setTextSize(...);
596      *         vh.textView.setFontVariationSettings(...);
597      *
598      *         // start precompute
599      *         Future<PrecomputedTextCompat> future = PrecomputedTextCompat.getTextFuture(
600      *                 data.text, vh.textView.getTextMetricsParamsCompat(), myExecutor);
601      *
602      *         // and pass future to TextView, which awaits result before measuring
603      *         vh.textView.setTextFuture(future);
604      *     }
605      * </pre>
606      * Because RecyclerView
607      * {@link androidx.recyclerview.widget.RecyclerView.LayoutManager#isItemPrefetchEnabled
608      * prefetches} bind multiple frames in advance while scrolling, the text work generally has
609      * plenty of time to complete before measurement occurs.
610      * </p>
611      * <p class="note">
612      *     <strong>Note:</strong> all TextView layout properties must be set before creating the
613      *     Params object. If they are changed during the precomputation, this can cause a
614      *     {@link IllegalArgumentException} when the precomputed value is consumed during measure,
615      *     and doesn't reflect the TextView's current state.
616      * </p>
617      * @param charSequence the text to be displayed
618      * @param params the parameters to be used for displaying text
619      * @param executor the executor to be process the text layout. If null is passed, the default
620      *                single threaded pool will be used.
621      * @return a future of the precomputed text
622      *
623      * @see androidx.appcompat.widget.AppCompatTextView#setTextFuture
624      */
625     @UiThread
getTextFuture( final @NonNull CharSequence charSequence, PrecomputedTextCompat.@NonNull Params params, @Nullable Executor executor)626     public static Future<PrecomputedTextCompat> getTextFuture(
627             final @NonNull CharSequence charSequence, PrecomputedTextCompat.@NonNull Params params,
628             @Nullable Executor executor) {
629         PrecomputedTextFutureTask task = new PrecomputedTextFutureTask(params, charSequence);
630         if (executor == null) {
631             synchronized (sLock) {
632                 if (sExecutor == null) {
633                     sExecutor = Executors.newFixedThreadPool(1);
634                 }
635                 executor = sExecutor;
636             }
637         }
638         executor.execute(task);
639         return task;
640     }
641 
642 
643     ///////////////////////////////////////////////////////////////////////////////////////////////
644     // Spannable overrides
645     //
646     // Do not allow to modify MetricAffectingSpan
647 
648     /**
649      * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified.
650      */
651     @Override
setSpan(Object what, int start, int end, int flags)652     public void setSpan(Object what, int start, int end, int flags) {
653         if (what instanceof MetricAffectingSpan) {
654             throw new IllegalArgumentException(
655                     "MetricAffectingSpan can not be set to PrecomputedText.");
656         }
657         if (Build.VERSION.SDK_INT >= 29) {
658             mWrapped.setSpan(what, start, end, flags);
659         } else {
660             mText.setSpan(what, start, end, flags);
661         }
662     }
663 
664     /**
665      * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified.
666      */
667     @Override
removeSpan(Object what)668     public void removeSpan(Object what) {
669         if (what instanceof MetricAffectingSpan) {
670             throw new IllegalArgumentException(
671                     "MetricAffectingSpan can not be removed from PrecomputedText.");
672         }
673         if (Build.VERSION.SDK_INT >= 29) {
674             mWrapped.removeSpan(what);
675         } else {
676             mText.removeSpan(what);
677         }
678     }
679 
680     ///////////////////////////////////////////////////////////////////////////////////////////////
681     // Spanned overrides
682     //
683     // Just proxy for underlying mText if appropriate.
684 
685     @Override
getSpans(int start, int end, Class<T> type)686     public <T> T[] getSpans(int start, int end, Class<T> type) {
687         if (Build.VERSION.SDK_INT >= 29) {
688             return mWrapped.getSpans(start, end, type);
689         } else {
690             return mText.getSpans(start, end, type);
691         }
692 
693     }
694 
695     @Override
getSpanStart(Object tag)696     public int getSpanStart(Object tag) {
697         return mText.getSpanStart(tag);
698     }
699 
700     @Override
getSpanEnd(Object tag)701     public int getSpanEnd(Object tag) {
702         return mText.getSpanEnd(tag);
703     }
704 
705     @Override
getSpanFlags(Object tag)706     public int getSpanFlags(Object tag) {
707         return mText.getSpanFlags(tag);
708     }
709 
710     @Override
nextSpanTransition(int start, int limit, Class type)711     public int nextSpanTransition(int start, int limit, Class type) {
712         return mText.nextSpanTransition(start, limit, type);
713     }
714 
715     ///////////////////////////////////////////////////////////////////////////////////////////////
716     // CharSequence overrides.
717     //
718     // Just proxy for underlying mText.
719 
720     @Override
length()721     public int length() {
722         return mText.length();
723     }
724 
725     @Override
charAt(int index)726     public char charAt(int index) {
727         return mText.charAt(index);
728     }
729 
730     @Override
subSequence(int start, int end)731     public CharSequence subSequence(int start, int end) {
732         return mText.subSequence(start, end);
733     }
734 
735     @Override
toString()736     public @NonNull String toString() {
737         return mText.toString();
738     }
739 
740     @RequiresApi(28)
741     static class Api28Impl {
Api28Impl()742         private Api28Impl() {
743             // This class is not instantiable.
744         }
745 
castToSpannable(PrecomputedText precomputedText)746         static Spannable castToSpannable(PrecomputedText precomputedText) {
747             return precomputedText;
748         }
749     }
750 }
751