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