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