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