1 /* 2 * Copyright (C) 2017 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 android.text; 18 19 import android.annotation.FloatRange; 20 import android.annotation.IntDef; 21 import android.annotation.IntRange; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.graphics.Rect; 25 import android.text.style.MetricAffectingSpan; 26 27 import com.android.internal.util.Preconditions; 28 29 import java.lang.annotation.Retention; 30 import java.lang.annotation.RetentionPolicy; 31 import java.util.ArrayList; 32 import java.util.Objects; 33 34 /** 35 * A text which has the character metrics data. 36 * 37 * A text object that contains the character metrics data and can be used to improve the performance 38 * of text layout operations. When a PrecomputedText is created with a given {@link CharSequence}, 39 * it will measure the text metrics during the creation. This PrecomputedText instance can be set on 40 * {@link android.widget.TextView} or {@link StaticLayout}. Since the text layout information will 41 * be included in this instance, {@link android.widget.TextView} or {@link StaticLayout} will not 42 * have to recalculate this information. 43 * 44 * Note that the {@link PrecomputedText} created from different parameters of the target {@link 45 * android.widget.TextView} will be rejected internally and compute the text layout again with the 46 * current {@link android.widget.TextView} parameters. 47 * 48 * <pre> 49 * An example usage is: 50 * <code> 51 * static void asyncSetText(TextView textView, final String longString, Executor bgExecutor) { 52 * // construct precompute related parameters using the TextView that we will set the text on. 53 * final PrecomputedText.Params params = textView.getTextMetricsParams(); 54 * final Reference textViewRef = new WeakReference<>(textView); 55 * bgExecutor.submit(() -> { 56 * TextView textView = textViewRef.get(); 57 * if (textView == null) return; 58 * final PrecomputedText precomputedText = PrecomputedText.create(longString, params); 59 * textView.post(() -> { 60 * TextView textView = textViewRef.get(); 61 * if (textView == null) return; 62 * textView.setText(precomputedText); 63 * }); 64 * }); 65 * } 66 * </code> 67 * </pre> 68 * 69 * Note that the {@link PrecomputedText} created from different parameters of the target 70 * {@link android.widget.TextView} will be rejected. 71 * 72 * Note that any {@link android.text.NoCopySpan} attached to the original text won't be passed to 73 * PrecomputedText. 74 */ 75 public class PrecomputedText implements Spannable { 76 private static final char LINE_FEED = '\n'; 77 78 /** 79 * The information required for building {@link PrecomputedText}. 80 * 81 * Contains information required for precomputing text measurement metadata, so it can be done 82 * in isolation of a {@link android.widget.TextView} or {@link StaticLayout}, when final layout 83 * constraints are not known. 84 */ 85 public static final class Params { 86 // The TextPaint used for measurement. 87 private final @NonNull TextPaint mPaint; 88 89 // The requested text direction. 90 private final @NonNull TextDirectionHeuristic mTextDir; 91 92 // The break strategy for this measured text. 93 private final @Layout.BreakStrategy int mBreakStrategy; 94 95 // The hyphenation frequency for this measured text. 96 private final @Layout.HyphenationFrequency int mHyphenationFrequency; 97 98 /** 99 * A builder for creating {@link Params}. 100 */ 101 public static class Builder { 102 // The TextPaint used for measurement. 103 private final @NonNull TextPaint mPaint; 104 105 // The requested text direction. 106 private TextDirectionHeuristic mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR; 107 108 // The break strategy for this measured text. 109 private @Layout.BreakStrategy int mBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY; 110 111 // The hyphenation frequency for this measured text. 112 private @Layout.HyphenationFrequency int mHyphenationFrequency = 113 Layout.HYPHENATION_FREQUENCY_NORMAL; 114 115 /** 116 * Builder constructor. 117 * 118 * @param paint the paint to be used for drawing 119 */ Builder(@onNull TextPaint paint)120 public Builder(@NonNull TextPaint paint) { 121 mPaint = paint; 122 } 123 124 /** 125 * Builder constructor from existing params. 126 */ Builder(@onNull Params params)127 public Builder(@NonNull Params params) { 128 mPaint = params.mPaint; 129 mTextDir = params.mTextDir; 130 mBreakStrategy = params.mBreakStrategy; 131 mHyphenationFrequency = params.mHyphenationFrequency; 132 } 133 134 /** 135 * Set the line break strategy. 136 * 137 * The default value is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}. 138 * 139 * @param strategy the break strategy 140 * @return this builder, useful for chaining 141 * @see StaticLayout.Builder#setBreakStrategy 142 * @see android.widget.TextView#setBreakStrategy 143 */ setBreakStrategy(@ayout.BreakStrategy int strategy)144 public Builder setBreakStrategy(@Layout.BreakStrategy int strategy) { 145 mBreakStrategy = strategy; 146 return this; 147 } 148 149 /** 150 * Set the hyphenation frequency. 151 * 152 * The default value is {@link Layout#HYPHENATION_FREQUENCY_NORMAL}. 153 * 154 * @param frequency the hyphenation frequency 155 * @return this builder, useful for chaining 156 * @see StaticLayout.Builder#setHyphenationFrequency 157 * @see android.widget.TextView#setHyphenationFrequency 158 */ setHyphenationFrequency(@ayout.HyphenationFrequency int frequency)159 public Builder setHyphenationFrequency(@Layout.HyphenationFrequency 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 * @param textDir the text direction heuristic for resolving bidi behavior 170 * @return this builder, useful for chaining 171 * @see StaticLayout.Builder#setTextDirection 172 */ setTextDirection(@onNull TextDirectionHeuristic textDir)173 public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) { 174 mTextDir = textDir; 175 return this; 176 } 177 178 /** 179 * Build the {@link Params}. 180 * 181 * @return the layout parameter 182 */ build()183 public @NonNull Params build() { 184 return new Params(mPaint, mTextDir, mBreakStrategy, mHyphenationFrequency); 185 } 186 } 187 188 // This is public hidden for internal use. 189 // For the external developers, use Builder instead. 190 /** @hide */ Params(@onNull TextPaint paint, @NonNull TextDirectionHeuristic textDir, @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency)191 public Params(@NonNull TextPaint paint, @NonNull TextDirectionHeuristic textDir, 192 @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency) { 193 mPaint = paint; 194 mTextDir = textDir; 195 mBreakStrategy = strategy; 196 mHyphenationFrequency = frequency; 197 } 198 199 /** 200 * Returns the {@link TextPaint} for this text. 201 * 202 * @return A {@link TextPaint} 203 */ getTextPaint()204 public @NonNull TextPaint getTextPaint() { 205 return mPaint; 206 } 207 208 /** 209 * Returns the {@link TextDirectionHeuristic} for this text. 210 * 211 * @return A {@link TextDirectionHeuristic} 212 */ getTextDirection()213 public @NonNull TextDirectionHeuristic getTextDirection() { 214 return mTextDir; 215 } 216 217 /** 218 * Returns the break strategy for this text. 219 * 220 * @return A line break strategy 221 */ getBreakStrategy()222 public @Layout.BreakStrategy int getBreakStrategy() { 223 return mBreakStrategy; 224 } 225 226 /** 227 * Returns the hyphenation frequency for this text. 228 * 229 * @return A hyphenation frequency 230 */ getHyphenationFrequency()231 public @Layout.HyphenationFrequency int getHyphenationFrequency() { 232 return mHyphenationFrequency; 233 } 234 235 /** @hide */ 236 @IntDef(value = { UNUSABLE, NEED_RECOMPUTE, USABLE }) 237 @Retention(RetentionPolicy.SOURCE) 238 public @interface CheckResultUsableResult {} 239 240 /** 241 * Constant for returning value of checkResultUsable indicating that given parameter is not 242 * compatible. 243 * @hide 244 */ 245 public static final int UNUSABLE = 0; 246 247 /** 248 * Constant for returning value of checkResultUsable indicating that given parameter is not 249 * compatible but partially usable for creating new PrecomputedText. 250 * @hide 251 */ 252 public static final int NEED_RECOMPUTE = 1; 253 254 /** 255 * Constant for returning value of checkResultUsable indicating that given parameter is 256 * compatible. 257 * @hide 258 */ 259 public static final int USABLE = 2; 260 261 /** @hide */ checkResultUsable(@onNull TextPaint paint, @NonNull TextDirectionHeuristic textDir, @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency)262 public @CheckResultUsableResult int checkResultUsable(@NonNull TextPaint paint, 263 @NonNull TextDirectionHeuristic textDir, @Layout.BreakStrategy int strategy, 264 @Layout.HyphenationFrequency int frequency) { 265 if (mBreakStrategy == strategy && mHyphenationFrequency == frequency 266 && mPaint.equalsForTextMeasurement(paint)) { 267 return mTextDir == textDir ? USABLE : NEED_RECOMPUTE; 268 } else { 269 return UNUSABLE; 270 } 271 } 272 273 /** 274 * Check if the same text layout. 275 * 276 * @return true if this and the given param result in the same text layout 277 */ 278 @Override equals(@ullable Object o)279 public boolean equals(@Nullable Object o) { 280 if (o == this) { 281 return true; 282 } 283 if (o == null || !(o instanceof Params)) { 284 return false; 285 } 286 Params param = (Params) o; 287 return checkResultUsable(param.mPaint, param.mTextDir, param.mBreakStrategy, 288 param.mHyphenationFrequency) == Params.USABLE; 289 } 290 291 @Override hashCode()292 public int hashCode() { 293 // TODO: implement MinikinPaint::hashCode and use it to keep consistency with equals. 294 return Objects.hash(mPaint.getTextSize(), mPaint.getTextScaleX(), mPaint.getTextSkewX(), 295 mPaint.getLetterSpacing(), mPaint.getWordSpacing(), mPaint.getFlags(), 296 mPaint.getTextLocales(), mPaint.getTypeface(), 297 mPaint.getFontVariationSettings(), mPaint.isElegantTextHeight(), mTextDir, 298 mBreakStrategy, mHyphenationFrequency); 299 } 300 301 @Override toString()302 public String toString() { 303 return "{" 304 + "textSize=" + mPaint.getTextSize() 305 + ", textScaleX=" + mPaint.getTextScaleX() 306 + ", textSkewX=" + mPaint.getTextSkewX() 307 + ", letterSpacing=" + mPaint.getLetterSpacing() 308 + ", textLocale=" + mPaint.getTextLocales() 309 + ", typeface=" + mPaint.getTypeface() 310 + ", variationSettings=" + mPaint.getFontVariationSettings() 311 + ", elegantTextHeight=" + mPaint.isElegantTextHeight() 312 + ", textDir=" + mTextDir 313 + ", breakStrategy=" + mBreakStrategy 314 + ", hyphenationFrequency=" + mHyphenationFrequency 315 + "}"; 316 } 317 }; 318 319 /** @hide */ 320 public static class ParagraphInfo { 321 public final @IntRange(from = 0) int paragraphEnd; 322 public final @NonNull MeasuredParagraph measured; 323 324 /** 325 * @param paraEnd the end offset of this paragraph 326 * @param measured a measured paragraph 327 */ ParagraphInfo(@ntRangefrom = 0) int paraEnd, @NonNull MeasuredParagraph measured)328 public ParagraphInfo(@IntRange(from = 0) int paraEnd, @NonNull MeasuredParagraph measured) { 329 this.paragraphEnd = paraEnd; 330 this.measured = measured; 331 } 332 }; 333 334 335 // The original text. 336 private final @NonNull SpannableString mText; 337 338 // The inclusive start offset of the measuring target. 339 private final @IntRange(from = 0) int mStart; 340 341 // The exclusive end offset of the measuring target. 342 private final @IntRange(from = 0) int mEnd; 343 344 private final @NonNull Params mParams; 345 346 // The list of measured paragraph info. 347 private final @NonNull ParagraphInfo[] mParagraphInfo; 348 349 /** 350 * Create a new {@link PrecomputedText} which will pre-compute text measurement and glyph 351 * positioning information. 352 * <p> 353 * This can be expensive, so computing this on a background thread before your text will be 354 * presented can save work on the UI thread. 355 * </p> 356 * 357 * Note that any {@link android.text.NoCopySpan} attached to the text won't be passed to the 358 * created PrecomputedText. 359 * 360 * @param text the text to be measured 361 * @param params parameters that define how text will be precomputed 362 * @return A {@link PrecomputedText} 363 */ create(@onNull CharSequence text, @NonNull Params params)364 public static PrecomputedText create(@NonNull CharSequence text, @NonNull Params params) { 365 ParagraphInfo[] paraInfo = null; 366 if (text instanceof PrecomputedText) { 367 final PrecomputedText hintPct = (PrecomputedText) text; 368 final PrecomputedText.Params hintParams = hintPct.getParams(); 369 final @Params.CheckResultUsableResult int checkResult = 370 hintParams.checkResultUsable(params.mPaint, params.mTextDir, 371 params.mBreakStrategy, params.mHyphenationFrequency); 372 switch (checkResult) { 373 case Params.USABLE: 374 return hintPct; 375 case Params.NEED_RECOMPUTE: 376 // To be able to use PrecomputedText for new params, at least break strategy and 377 // hyphenation frequency must be the same. 378 if (params.getBreakStrategy() == hintParams.getBreakStrategy() 379 && params.getHyphenationFrequency() 380 == hintParams.getHyphenationFrequency()) { 381 paraInfo = createMeasuredParagraphsFromPrecomputedText( 382 hintPct, params, true /* compute layout */); 383 } 384 break; 385 case Params.UNUSABLE: 386 // Unable to use anything in PrecomputedText. Create PrecomputedText as the 387 // normal text input. 388 } 389 390 } 391 if (paraInfo == null) { 392 paraInfo = createMeasuredParagraphs( 393 text, params, 0, text.length(), true /* computeLayout */); 394 } 395 return new PrecomputedText(text, 0, text.length(), params, paraInfo); 396 } 397 createMeasuredParagraphsFromPrecomputedText( @onNull PrecomputedText pct, @NonNull Params params, boolean computeLayout)398 private static ParagraphInfo[] createMeasuredParagraphsFromPrecomputedText( 399 @NonNull PrecomputedText pct, @NonNull Params params, boolean computeLayout) { 400 final boolean needHyphenation = params.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE 401 && params.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE; 402 ArrayList<ParagraphInfo> result = new ArrayList<>(); 403 for (int i = 0; i < pct.getParagraphCount(); ++i) { 404 final int paraStart = pct.getParagraphStart(i); 405 final int paraEnd = pct.getParagraphEnd(i); 406 result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout( 407 params.getTextPaint(), pct, paraStart, paraEnd, params.getTextDirection(), 408 needHyphenation, computeLayout, pct.getMeasuredParagraph(i), 409 null /* no recycle */))); 410 } 411 return result.toArray(new ParagraphInfo[result.size()]); 412 } 413 414 /** @hide */ createMeasuredParagraphs( @onNull CharSequence text, @NonNull Params params, @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean computeLayout)415 public static ParagraphInfo[] createMeasuredParagraphs( 416 @NonNull CharSequence text, @NonNull Params params, 417 @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean computeLayout) { 418 ArrayList<ParagraphInfo> result = new ArrayList<>(); 419 420 Preconditions.checkNotNull(text); 421 Preconditions.checkNotNull(params); 422 final boolean needHyphenation = params.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE 423 && params.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE; 424 425 int paraEnd = 0; 426 for (int paraStart = start; paraStart < end; paraStart = paraEnd) { 427 paraEnd = TextUtils.indexOf(text, LINE_FEED, paraStart, end); 428 if (paraEnd < 0) { 429 // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph 430 // end. 431 paraEnd = end; 432 } else { 433 paraEnd++; // Includes LINE_FEED(U+000A) to the prev paragraph. 434 } 435 436 result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout( 437 params.getTextPaint(), text, paraStart, paraEnd, params.getTextDirection(), 438 needHyphenation, computeLayout, null /* no hint */, 439 null /* no recycle */))); 440 } 441 return result.toArray(new ParagraphInfo[result.size()]); 442 } 443 444 // Use PrecomputedText.create instead. PrecomputedText(@onNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull Params params, @NonNull ParagraphInfo[] paraInfo)445 private PrecomputedText(@NonNull CharSequence text, @IntRange(from = 0) int start, 446 @IntRange(from = 0) int end, @NonNull Params params, 447 @NonNull ParagraphInfo[] paraInfo) { 448 mText = new SpannableString(text, true /* ignoreNoCopySpan */); 449 mStart = start; 450 mEnd = end; 451 mParams = params; 452 mParagraphInfo = paraInfo; 453 } 454 455 /** 456 * Return the underlying text. 457 * @hide 458 */ getText()459 public @NonNull CharSequence getText() { 460 return mText; 461 } 462 463 /** 464 * Returns the inclusive start offset of measured region. 465 * @hide 466 */ getStart()467 public @IntRange(from = 0) int getStart() { 468 return mStart; 469 } 470 471 /** 472 * Returns the exclusive end offset of measured region. 473 * @hide 474 */ getEnd()475 public @IntRange(from = 0) int getEnd() { 476 return mEnd; 477 } 478 479 /** 480 * Returns the layout parameters used to measure this text. 481 */ getParams()482 public @NonNull Params getParams() { 483 return mParams; 484 } 485 486 /** 487 * Returns the count of paragraphs. 488 */ getParagraphCount()489 public @IntRange(from = 0) int getParagraphCount() { 490 return mParagraphInfo.length; 491 } 492 493 /** 494 * Returns the paragraph start offset of the text. 495 */ getParagraphStart(@ntRangefrom = 0) int paraIndex)496 public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) { 497 Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex"); 498 return paraIndex == 0 ? mStart : getParagraphEnd(paraIndex - 1); 499 } 500 501 /** 502 * Returns the paragraph end offset of the text. 503 */ getParagraphEnd(@ntRangefrom = 0) int paraIndex)504 public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) { 505 Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex"); 506 return mParagraphInfo[paraIndex].paragraphEnd; 507 } 508 509 /** @hide */ getMeasuredParagraph(@ntRangefrom = 0) int paraIndex)510 public @NonNull MeasuredParagraph getMeasuredParagraph(@IntRange(from = 0) int paraIndex) { 511 return mParagraphInfo[paraIndex].measured; 512 } 513 514 /** @hide */ getParagraphInfo()515 public @NonNull ParagraphInfo[] getParagraphInfo() { 516 return mParagraphInfo; 517 } 518 519 /** 520 * Returns true if the given TextPaint gives the same result of text layout for this text. 521 * @hide 522 */ checkResultUsable(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir, @NonNull TextPaint paint, @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency)523 public @Params.CheckResultUsableResult int checkResultUsable(@IntRange(from = 0) int start, 524 @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir, 525 @NonNull TextPaint paint, @Layout.BreakStrategy int strategy, 526 @Layout.HyphenationFrequency int frequency) { 527 if (mStart != start || mEnd != end) { 528 return Params.UNUSABLE; 529 } else { 530 return mParams.checkResultUsable(paint, textDir, strategy, frequency); 531 } 532 } 533 534 /** @hide */ findParaIndex(@ntRangefrom = 0) int pos)535 public int findParaIndex(@IntRange(from = 0) int pos) { 536 // TODO: Maybe good to remove paragraph concept from PrecomputedText and add substring 537 // layout support to StaticLayout. 538 for (int i = 0; i < mParagraphInfo.length; ++i) { 539 if (pos < mParagraphInfo[i].paragraphEnd) { 540 return i; 541 } 542 } 543 throw new IndexOutOfBoundsException( 544 "pos must be less than " + mParagraphInfo[mParagraphInfo.length - 1].paragraphEnd 545 + ", gave " + pos); 546 } 547 548 /** 549 * Returns text width for the given range. 550 * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise 551 * IllegalArgumentException will be thrown. 552 * 553 * @param start the inclusive start offset in the text 554 * @param end the exclusive end offset in the text 555 * @return the text width 556 * @throws IllegalArgumentException if start and end offset are in the different paragraph. 557 */ getWidth(@ntRangefrom = 0) int start, @IntRange(from = 0) int end)558 public @FloatRange(from = 0) float getWidth(@IntRange(from = 0) int start, 559 @IntRange(from = 0) int end) { 560 Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset"); 561 Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset"); 562 Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset"); 563 564 if (start == end) { 565 return 0; 566 } 567 final int paraIndex = findParaIndex(start); 568 final int paraStart = getParagraphStart(paraIndex); 569 final int paraEnd = getParagraphEnd(paraIndex); 570 if (start < paraStart || paraEnd < end) { 571 throw new IllegalArgumentException("Cannot measured across the paragraph:" 572 + "para: (" + paraStart + ", " + paraEnd + "), " 573 + "request: (" + start + ", " + end + ")"); 574 } 575 return getMeasuredParagraph(paraIndex).getWidth(start - paraStart, end - paraStart); 576 } 577 578 /** 579 * Retrieves the text bounding box for the given range. 580 * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise 581 * IllegalArgumentException will be thrown. 582 * 583 * @param start the inclusive start offset in the text 584 * @param end the exclusive end offset in the text 585 * @param bounds the output rectangle 586 * @throws IllegalArgumentException if start and end offset are in the different paragraph. 587 */ getBounds(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull Rect bounds)588 public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end, 589 @NonNull Rect bounds) { 590 Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset"); 591 Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset"); 592 Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset"); 593 Preconditions.checkNotNull(bounds); 594 if (start == end) { 595 bounds.set(0, 0, 0, 0); 596 return; 597 } 598 final int paraIndex = findParaIndex(start); 599 final int paraStart = getParagraphStart(paraIndex); 600 final int paraEnd = getParagraphEnd(paraIndex); 601 if (start < paraStart || paraEnd < end) { 602 throw new IllegalArgumentException("Cannot measured across the paragraph:" 603 + "para: (" + paraStart + ", " + paraEnd + "), " 604 + "request: (" + start + ", " + end + ")"); 605 } 606 getMeasuredParagraph(paraIndex).getBounds(start - paraStart, end - paraStart, bounds); 607 } 608 609 /** 610 * Returns a width of a character at offset 611 * 612 * @param offset an offset of the text. 613 * @return a width of the character. 614 * @hide 615 */ getCharWidthAt(@ntRangefrom = 0) int offset)616 public float getCharWidthAt(@IntRange(from = 0) int offset) { 617 Preconditions.checkArgument(0 <= offset && offset < mText.length(), "invalid offset"); 618 final int paraIndex = findParaIndex(offset); 619 final int paraStart = getParagraphStart(paraIndex); 620 final int paraEnd = getParagraphEnd(paraIndex); 621 return getMeasuredParagraph(paraIndex).getCharWidthAt(offset - paraStart); 622 } 623 624 /** 625 * Returns the size of native PrecomputedText memory usage. 626 * 627 * Note that this is not guaranteed to be accurate. Must be used only for testing purposes. 628 * @hide 629 */ getMemoryUsage()630 public int getMemoryUsage() { 631 int r = 0; 632 for (int i = 0; i < getParagraphCount(); ++i) { 633 r += getMeasuredParagraph(i).getMemoryUsage(); 634 } 635 return r; 636 } 637 638 /////////////////////////////////////////////////////////////////////////////////////////////// 639 // Spannable overrides 640 // 641 // Do not allow to modify MetricAffectingSpan 642 643 /** 644 * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified. 645 */ 646 @Override setSpan(Object what, int start, int end, int flags)647 public void setSpan(Object what, int start, int end, int flags) { 648 if (what instanceof MetricAffectingSpan) { 649 throw new IllegalArgumentException( 650 "MetricAffectingSpan can not be set to PrecomputedText."); 651 } 652 mText.setSpan(what, start, end, flags); 653 } 654 655 /** 656 * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified. 657 */ 658 @Override removeSpan(Object what)659 public void removeSpan(Object what) { 660 if (what instanceof MetricAffectingSpan) { 661 throw new IllegalArgumentException( 662 "MetricAffectingSpan can not be removed from PrecomputedText."); 663 } 664 mText.removeSpan(what); 665 } 666 667 /////////////////////////////////////////////////////////////////////////////////////////////// 668 // Spanned overrides 669 // 670 // Just proxy for underlying mText if appropriate. 671 672 @Override getSpans(int start, int end, Class<T> type)673 public <T> T[] getSpans(int start, int end, Class<T> type) { 674 return mText.getSpans(start, end, type); 675 } 676 677 @Override getSpanStart(Object tag)678 public int getSpanStart(Object tag) { 679 return mText.getSpanStart(tag); 680 } 681 682 @Override getSpanEnd(Object tag)683 public int getSpanEnd(Object tag) { 684 return mText.getSpanEnd(tag); 685 } 686 687 @Override getSpanFlags(Object tag)688 public int getSpanFlags(Object tag) { 689 return mText.getSpanFlags(tag); 690 } 691 692 @Override nextSpanTransition(int start, int limit, Class type)693 public int nextSpanTransition(int start, int limit, Class type) { 694 return mText.nextSpanTransition(start, limit, type); 695 } 696 697 /////////////////////////////////////////////////////////////////////////////////////////////// 698 // CharSequence overrides. 699 // 700 // Just proxy for underlying mText. 701 702 @Override length()703 public int length() { 704 return mText.length(); 705 } 706 707 @Override charAt(int index)708 public char charAt(int index) { 709 return mText.charAt(index); 710 } 711 712 @Override subSequence(int start, int end)713 public CharSequence subSequence(int start, int end) { 714 return PrecomputedText.create(mText.subSequence(start, end), mParams); 715 } 716 717 @Override toString()718 public String toString() { 719 return mText.toString(); 720 } 721 } 722