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 return Objects.hash(mPaint.getTextSize(), mPaint.getTextScaleX(), mPaint.getTextSkewX(), 333 mPaint.getLetterSpacing(), mPaint.getWordSpacing(), mPaint.getFlags(), 334 mPaint.getTextLocales(), mPaint.getTypeface(), 335 mPaint.getFontVariationSettings(), mPaint.isElegantTextHeight(), mTextDir, 336 mBreakStrategy, mHyphenationFrequency, 337 LineBreakConfig.getResolvedLineBreakStyle(mLineBreakConfig), 338 LineBreakConfig.getResolvedLineBreakWordStyle(mLineBreakConfig)); 339 } 340 341 @Override toString()342 public String toString() { 343 return "{" 344 + "textSize=" + mPaint.getTextSize() 345 + ", textScaleX=" + mPaint.getTextScaleX() 346 + ", textSkewX=" + mPaint.getTextSkewX() 347 + ", letterSpacing=" + mPaint.getLetterSpacing() 348 + ", textLocale=" + mPaint.getTextLocales() 349 + ", typeface=" + mPaint.getTypeface() 350 + ", variationSettings=" + mPaint.getFontVariationSettings() 351 + ", elegantTextHeight=" + mPaint.isElegantTextHeight() 352 + ", textDir=" + mTextDir 353 + ", breakStrategy=" + mBreakStrategy 354 + ", hyphenationFrequency=" + mHyphenationFrequency 355 + ", lineBreakStyle=" + LineBreakConfig.getResolvedLineBreakStyle(mLineBreakConfig) 356 + ", lineBreakWordStyle=" 357 + LineBreakConfig.getResolvedLineBreakWordStyle(mLineBreakConfig) 358 + "}"; 359 } 360 }; 361 362 /** @hide */ 363 public static class ParagraphInfo { 364 public final @IntRange(from = 0) int paragraphEnd; 365 public final @NonNull MeasuredParagraph measured; 366 367 /** 368 * @param paraEnd the end offset of this paragraph 369 * @param measured a measured paragraph 370 */ ParagraphInfo(@ntRangefrom = 0) int paraEnd, @NonNull MeasuredParagraph measured)371 public ParagraphInfo(@IntRange(from = 0) int paraEnd, @NonNull MeasuredParagraph measured) { 372 this.paragraphEnd = paraEnd; 373 this.measured = measured; 374 } 375 }; 376 377 378 // The original text. 379 private final @NonNull SpannableString mText; 380 381 // The inclusive start offset of the measuring target. 382 private final @IntRange(from = 0) int mStart; 383 384 // The exclusive end offset of the measuring target. 385 private final @IntRange(from = 0) int mEnd; 386 387 private final @NonNull Params mParams; 388 389 // The list of measured paragraph info. 390 private final @NonNull ParagraphInfo[] mParagraphInfo; 391 392 /** 393 * Create a new {@link PrecomputedText} which will pre-compute text measurement and glyph 394 * positioning information. 395 * <p> 396 * This can be expensive, so computing this on a background thread before your text will be 397 * presented can save work on the UI thread. 398 * </p> 399 * 400 * Note that any {@link android.text.NoCopySpan} attached to the text won't be passed to the 401 * created PrecomputedText. 402 * 403 * @param text the text to be measured 404 * @param params parameters that define how text will be precomputed 405 * @return A {@link PrecomputedText} 406 */ create(@onNull CharSequence text, @NonNull Params params)407 public static PrecomputedText create(@NonNull CharSequence text, @NonNull Params params) { 408 ParagraphInfo[] paraInfo = null; 409 if (text instanceof PrecomputedText) { 410 final PrecomputedText hintPct = (PrecomputedText) text; 411 final PrecomputedText.Params hintParams = hintPct.getParams(); 412 final @Params.CheckResultUsableResult int checkResult = 413 hintParams.checkResultUsable(params.mPaint, params.mTextDir, 414 params.mBreakStrategy, params.mHyphenationFrequency, 415 params.mLineBreakConfig); 416 switch (checkResult) { 417 case Params.USABLE: 418 return hintPct; 419 case Params.NEED_RECOMPUTE: 420 // To be able to use PrecomputedText for new params, at least break strategy and 421 // hyphenation frequency must be the same. 422 if (params.getBreakStrategy() == hintParams.getBreakStrategy() 423 && params.getHyphenationFrequency() 424 == hintParams.getHyphenationFrequency()) { 425 paraInfo = createMeasuredParagraphsFromPrecomputedText( 426 hintPct, params, true /* compute layout */); 427 } 428 break; 429 case Params.UNUSABLE: 430 // Unable to use anything in PrecomputedText. Create PrecomputedText as the 431 // normal text input. 432 } 433 434 } 435 if (paraInfo == null) { 436 paraInfo = createMeasuredParagraphs( 437 text, params, 0, text.length(), true /* computeLayout */, 438 true /* computeBounds */); 439 } 440 return new PrecomputedText(text, 0, text.length(), params, paraInfo); 441 } 442 isFastHyphenation(int frequency)443 private static boolean isFastHyphenation(int frequency) { 444 return frequency == Layout.HYPHENATION_FREQUENCY_FULL_FAST 445 || frequency == Layout.HYPHENATION_FREQUENCY_NORMAL_FAST; 446 } 447 createMeasuredParagraphsFromPrecomputedText( @onNull PrecomputedText pct, @NonNull Params params, boolean computeLayout)448 private static ParagraphInfo[] createMeasuredParagraphsFromPrecomputedText( 449 @NonNull PrecomputedText pct, @NonNull Params params, boolean computeLayout) { 450 final boolean needHyphenation = params.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE 451 && params.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE; 452 final int hyphenationMode; 453 if (needHyphenation) { 454 hyphenationMode = isFastHyphenation(params.getHyphenationFrequency()) 455 ? MeasuredText.Builder.HYPHENATION_MODE_FAST : 456 MeasuredText.Builder.HYPHENATION_MODE_NORMAL; 457 } else { 458 hyphenationMode = MeasuredText.Builder.HYPHENATION_MODE_NONE; 459 } 460 LineBreakConfig config = params.getLineBreakConfig(); 461 if (config.getLineBreakWordStyle() == LineBreakConfig.LINE_BREAK_WORD_STYLE_AUTO 462 && pct.getParagraphCount() != 1) { 463 // If the text has multiple paragraph, resolve line break word style auto to none. 464 config = new LineBreakConfig.Builder() 465 .merge(config) 466 .setLineBreakWordStyle(LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE) 467 .build(); 468 } 469 ArrayList<ParagraphInfo> result = new ArrayList<>(); 470 for (int i = 0; i < pct.getParagraphCount(); ++i) { 471 final int paraStart = pct.getParagraphStart(i); 472 final int paraEnd = pct.getParagraphEnd(i); 473 result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout( 474 params.getTextPaint(), config, pct, paraStart, paraEnd, 475 params.getTextDirection(), hyphenationMode, computeLayout, true, 476 pct.getMeasuredParagraph(i), null /* no recycle */))); 477 } 478 return result.toArray(new ParagraphInfo[result.size()]); 479 } 480 481 /** @hide */ createMeasuredParagraphs( @onNull CharSequence text, @NonNull Params params, @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean computeLayout, boolean computeBounds)482 public static ParagraphInfo[] createMeasuredParagraphs( 483 @NonNull CharSequence text, @NonNull Params params, 484 @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean computeLayout, 485 boolean computeBounds) { 486 ArrayList<ParagraphInfo> result = new ArrayList<>(); 487 488 Preconditions.checkNotNull(text); 489 Preconditions.checkNotNull(params); 490 final boolean needHyphenation = params.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE 491 && params.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE; 492 final int hyphenationMode; 493 if (needHyphenation) { 494 hyphenationMode = isFastHyphenation(params.getHyphenationFrequency()) 495 ? MeasuredText.Builder.HYPHENATION_MODE_FAST : 496 MeasuredText.Builder.HYPHENATION_MODE_NORMAL; 497 } else { 498 hyphenationMode = MeasuredText.Builder.HYPHENATION_MODE_NONE; 499 } 500 501 LineBreakConfig config = null; 502 int paraEnd = 0; 503 for (int paraStart = start; paraStart < end; paraStart = paraEnd) { 504 paraEnd = TextUtils.indexOf(text, LINE_FEED, paraStart, end); 505 if (paraEnd < 0) { 506 // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph 507 // end. 508 paraEnd = end; 509 } else { 510 paraEnd++; // Includes LINE_FEED(U+000A) to the prev paragraph. 511 } 512 513 if (config == null) { 514 config = params.getLineBreakConfig(); 515 if (config.getLineBreakWordStyle() == LineBreakConfig.LINE_BREAK_WORD_STYLE_AUTO 516 && !(paraStart == start && paraEnd == end)) { 517 // If the text has multiple paragraph, resolve line break word style auto to 518 // none. 519 config = new LineBreakConfig.Builder() 520 .merge(config) 521 .setLineBreakWordStyle(LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE) 522 .build(); 523 } 524 } 525 526 result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout( 527 params.getTextPaint(), config, text, paraStart, paraEnd, 528 params.getTextDirection(), hyphenationMode, computeLayout, computeBounds, 529 null /* no hint */, 530 null /* no recycle */))); 531 } 532 return result.toArray(new ParagraphInfo[result.size()]); 533 } 534 535 // Use PrecomputedText.create instead. PrecomputedText(@onNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull Params params, @NonNull ParagraphInfo[] paraInfo)536 private PrecomputedText(@NonNull CharSequence text, @IntRange(from = 0) int start, 537 @IntRange(from = 0) int end, @NonNull Params params, 538 @NonNull ParagraphInfo[] paraInfo) { 539 mText = new SpannableString(text, true /* ignoreNoCopySpan */); 540 mStart = start; 541 mEnd = end; 542 mParams = params; 543 mParagraphInfo = paraInfo; 544 } 545 546 /** 547 * Return the underlying text. 548 * @hide 549 */ getText()550 public @NonNull CharSequence getText() { 551 return mText; 552 } 553 554 /** 555 * Returns the inclusive start offset of measured region. 556 * @hide 557 */ getStart()558 public @IntRange(from = 0) int getStart() { 559 return mStart; 560 } 561 562 /** 563 * Returns the exclusive end offset of measured region. 564 * @hide 565 */ getEnd()566 public @IntRange(from = 0) int getEnd() { 567 return mEnd; 568 } 569 570 /** 571 * Returns the layout parameters used to measure this text. 572 */ getParams()573 public @NonNull Params getParams() { 574 return mParams; 575 } 576 577 /** 578 * Returns the count of paragraphs. 579 */ getParagraphCount()580 public @IntRange(from = 0) int getParagraphCount() { 581 return mParagraphInfo.length; 582 } 583 584 /** 585 * Returns the paragraph start offset of the text. 586 */ getParagraphStart(@ntRangefrom = 0) int paraIndex)587 public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) { 588 Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex"); 589 return paraIndex == 0 ? mStart : getParagraphEnd(paraIndex - 1); 590 } 591 592 /** 593 * Returns the paragraph end offset of the text. 594 */ getParagraphEnd(@ntRangefrom = 0) int paraIndex)595 public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) { 596 Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex"); 597 return mParagraphInfo[paraIndex].paragraphEnd; 598 } 599 600 /** @hide */ getMeasuredParagraph(@ntRangefrom = 0) int paraIndex)601 public @NonNull MeasuredParagraph getMeasuredParagraph(@IntRange(from = 0) int paraIndex) { 602 return mParagraphInfo[paraIndex].measured; 603 } 604 605 /** @hide */ getParagraphInfo()606 public @NonNull ParagraphInfo[] getParagraphInfo() { 607 return mParagraphInfo; 608 } 609 610 /** 611 * Returns true if the given TextPaint gives the same result of text layout for this text. 612 * @hide 613 */ 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)614 public @Params.CheckResultUsableResult int checkResultUsable(@IntRange(from = 0) int start, 615 @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir, 616 @NonNull TextPaint paint, @Layout.BreakStrategy int strategy, 617 @Layout.HyphenationFrequency int frequency, @NonNull LineBreakConfig lbConfig) { 618 if (mStart != start || mEnd != end) { 619 return Params.UNUSABLE; 620 } else { 621 return mParams.checkResultUsable(paint, textDir, strategy, frequency, lbConfig); 622 } 623 } 624 625 /** @hide */ findParaIndex(@ntRangefrom = 0) int pos)626 public int findParaIndex(@IntRange(from = 0) int pos) { 627 // TODO: Maybe good to remove paragraph concept from PrecomputedText and add substring 628 // layout support to StaticLayout. 629 for (int i = 0; i < mParagraphInfo.length; ++i) { 630 if (pos < mParagraphInfo[i].paragraphEnd) { 631 return i; 632 } 633 } 634 throw new IndexOutOfBoundsException( 635 "pos must be less than " + mParagraphInfo[mParagraphInfo.length - 1].paragraphEnd 636 + ", gave " + pos); 637 } 638 639 /** 640 * Returns text width for the given range. 641 * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise 642 * IllegalArgumentException will be thrown. 643 * 644 * @param start the inclusive start offset in the text 645 * @param end the exclusive end offset in the text 646 * @return the text width 647 * @throws IllegalArgumentException if start and end offset are in the different paragraph. 648 */ getWidth(@ntRangefrom = 0) int start, @IntRange(from = 0) int end)649 public @FloatRange(from = 0) float getWidth(@IntRange(from = 0) int start, 650 @IntRange(from = 0) int end) { 651 Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset"); 652 Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset"); 653 Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset"); 654 655 if (start == end) { 656 return 0; 657 } 658 final int paraIndex = findParaIndex(start); 659 final int paraStart = getParagraphStart(paraIndex); 660 final int paraEnd = getParagraphEnd(paraIndex); 661 if (start < paraStart || paraEnd < end) { 662 throw new IllegalArgumentException("Cannot measured across the paragraph:" 663 + "para: (" + paraStart + ", " + paraEnd + "), " 664 + "request: (" + start + ", " + end + ")"); 665 } 666 return getMeasuredParagraph(paraIndex).getWidth(start - paraStart, end - paraStart); 667 } 668 669 /** 670 * Retrieves the text bounding box for the given range. 671 * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise 672 * IllegalArgumentException will be thrown. 673 * 674 * @param start the inclusive start offset in the text 675 * @param end the exclusive end offset in the text 676 * @param bounds the output rectangle 677 * @throws IllegalArgumentException if start and end offset are in the different paragraph. 678 */ getBounds(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull Rect bounds)679 public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end, 680 @NonNull Rect bounds) { 681 Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset"); 682 Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset"); 683 Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset"); 684 Preconditions.checkNotNull(bounds); 685 if (start == end) { 686 bounds.set(0, 0, 0, 0); 687 return; 688 } 689 final int paraIndex = findParaIndex(start); 690 final int paraStart = getParagraphStart(paraIndex); 691 final int paraEnd = getParagraphEnd(paraIndex); 692 if (start < paraStart || paraEnd < end) { 693 throw new IllegalArgumentException("Cannot measured across the paragraph:" 694 + "para: (" + paraStart + ", " + paraEnd + "), " 695 + "request: (" + start + ", " + end + ")"); 696 } 697 getMeasuredParagraph(paraIndex).getBounds(start - paraStart, end - paraStart, bounds); 698 } 699 700 /** 701 * Retrieves the text font metrics for the given range. 702 * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise 703 * IllegalArgumentException will be thrown. 704 * 705 * @param start the inclusive start offset in the text 706 * @param end the exclusive end offset in the text 707 * @param outMetrics the output font metrics 708 * @throws IllegalArgumentException if start and end offset are in the different paragraph. 709 */ getFontMetricsInt(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull Paint.FontMetricsInt outMetrics)710 public void getFontMetricsInt(@IntRange(from = 0) int start, @IntRange(from = 0) int end, 711 @NonNull Paint.FontMetricsInt outMetrics) { 712 Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset"); 713 Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset"); 714 Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset"); 715 Objects.requireNonNull(outMetrics); 716 if (start == end) { 717 mParams.getTextPaint().getFontMetricsInt(outMetrics); 718 return; 719 } 720 final int paraIndex = findParaIndex(start); 721 final int paraStart = getParagraphStart(paraIndex); 722 final int paraEnd = getParagraphEnd(paraIndex); 723 if (start < paraStart || paraEnd < end) { 724 throw new IllegalArgumentException("Cannot measured across the paragraph:" 725 + "para: (" + paraStart + ", " + paraEnd + "), " 726 + "request: (" + start + ", " + end + ")"); 727 } 728 getMeasuredParagraph(paraIndex).getFontMetricsInt(start - paraStart, 729 end - paraStart, outMetrics); 730 } 731 732 /** 733 * Returns a width of a character at offset 734 * 735 * @param offset an offset of the text. 736 * @return a width of the character. 737 * @hide 738 */ getCharWidthAt(@ntRangefrom = 0) int offset)739 public float getCharWidthAt(@IntRange(from = 0) int offset) { 740 Preconditions.checkArgument(0 <= offset && offset < mText.length(), "invalid offset"); 741 final int paraIndex = findParaIndex(offset); 742 final int paraStart = getParagraphStart(paraIndex); 743 final int paraEnd = getParagraphEnd(paraIndex); 744 return getMeasuredParagraph(paraIndex).getCharWidthAt(offset - paraStart); 745 } 746 747 /** 748 * Returns the size of native PrecomputedText memory usage. 749 * 750 * Note that this is not guaranteed to be accurate. Must be used only for testing purposes. 751 * @hide 752 */ getMemoryUsage()753 public int getMemoryUsage() { 754 int r = 0; 755 for (int i = 0; i < getParagraphCount(); ++i) { 756 r += getMeasuredParagraph(i).getMemoryUsage(); 757 } 758 return r; 759 } 760 761 /////////////////////////////////////////////////////////////////////////////////////////////// 762 // Spannable overrides 763 // 764 // Do not allow to modify MetricAffectingSpan 765 766 /** 767 * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified. 768 */ 769 @Override setSpan(Object what, int start, int end, int flags)770 public void setSpan(Object what, int start, int end, int flags) { 771 if (what instanceof MetricAffectingSpan) { 772 throw new IllegalArgumentException( 773 "MetricAffectingSpan can not be set to PrecomputedText."); 774 } 775 mText.setSpan(what, start, end, flags); 776 } 777 778 /** 779 * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified. 780 */ 781 @Override removeSpan(Object what)782 public void removeSpan(Object what) { 783 if (what instanceof MetricAffectingSpan) { 784 throw new IllegalArgumentException( 785 "MetricAffectingSpan can not be removed from PrecomputedText."); 786 } 787 mText.removeSpan(what); 788 } 789 790 /////////////////////////////////////////////////////////////////////////////////////////////// 791 // Spanned overrides 792 // 793 // Just proxy for underlying mText if appropriate. 794 795 @Override getSpans(int start, int end, Class<T> type)796 public <T> T[] getSpans(int start, int end, Class<T> type) { 797 return mText.getSpans(start, end, type); 798 } 799 800 @Override getSpanStart(Object tag)801 public int getSpanStart(Object tag) { 802 return mText.getSpanStart(tag); 803 } 804 805 @Override getSpanEnd(Object tag)806 public int getSpanEnd(Object tag) { 807 return mText.getSpanEnd(tag); 808 } 809 810 @Override getSpanFlags(Object tag)811 public int getSpanFlags(Object tag) { 812 return mText.getSpanFlags(tag); 813 } 814 815 @Override nextSpanTransition(int start, int limit, Class type)816 public int nextSpanTransition(int start, int limit, Class type) { 817 return mText.nextSpanTransition(start, limit, type); 818 } 819 820 /////////////////////////////////////////////////////////////////////////////////////////////// 821 // CharSequence overrides. 822 // 823 // Just proxy for underlying mText. 824 825 @Override length()826 public int length() { 827 return mText.length(); 828 } 829 830 @Override charAt(int index)831 public char charAt(int index) { 832 return mText.charAt(index); 833 } 834 835 @Override subSequence(int start, int end)836 public CharSequence subSequence(int start, int end) { 837 return PrecomputedText.create(mText.subSequence(start, end), mParams); 838 } 839 840 @Override toString()841 public String toString() { 842 return mText.toString(); 843 } 844 } 845