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