1 /* 2 * Copyright (C) 2010 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.IntRange; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.graphics.Paint; 24 import android.graphics.Rect; 25 import android.graphics.text.LineBreakConfig; 26 import android.graphics.text.MeasuredText; 27 import android.text.AutoGrowArray.ByteArray; 28 import android.text.AutoGrowArray.FloatArray; 29 import android.text.AutoGrowArray.IntArray; 30 import android.text.Layout.Directions; 31 import android.text.style.MetricAffectingSpan; 32 import android.text.style.ReplacementSpan; 33 import android.util.Pools.SynchronizedPool; 34 35 import java.util.Arrays; 36 37 /** 38 * MeasuredParagraph provides text information for rendering purpose. 39 * 40 * The first motivation of this class is identify the text directions and retrieving individual 41 * character widths. However retrieving character widths is slower than identifying text directions. 42 * Thus, this class provides several builder methods for specific purposes. 43 * 44 * - buildForBidi: 45 * Compute only text directions. 46 * - buildForMeasurement: 47 * Compute text direction and all character widths. 48 * - buildForStaticLayout: 49 * This is bit special. StaticLayout also needs to know text direction and character widths for 50 * line breaking, but all things are done in native code. Similarly, text measurement is done 51 * in native code. So instead of storing result to Java array, this keeps the result in native 52 * code since there is no good reason to move the results to Java layer. 53 * 54 * In addition to the character widths, some additional information is computed for each purposes, 55 * e.g. whole text length for measurement or font metrics for static layout. 56 * 57 * MeasuredParagraph is NOT a thread safe object. 58 * @hide 59 */ 60 public class MeasuredParagraph { 61 private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC'; 62 MeasuredParagraph()63 private MeasuredParagraph() {} // Use build static functions instead. 64 65 private static final SynchronizedPool<MeasuredParagraph> sPool = new SynchronizedPool<>(1); 66 obtain()67 private static @NonNull MeasuredParagraph obtain() { // Use build static functions instead. 68 final MeasuredParagraph mt = sPool.acquire(); 69 return mt != null ? mt : new MeasuredParagraph(); 70 } 71 72 /** 73 * Recycle the MeasuredParagraph. 74 * 75 * Do not call any methods after you call this method. 76 */ recycle()77 public void recycle() { 78 release(); 79 sPool.release(this); 80 } 81 82 // The casted original text. 83 // 84 // This may be null if the passed text is not a Spanned. 85 private @Nullable Spanned mSpanned; 86 87 // The start offset of the target range in the original text (mSpanned); 88 private @IntRange(from = 0) int mTextStart; 89 90 // The length of the target range in the original text. 91 private @IntRange(from = 0) int mTextLength; 92 93 // The copied character buffer for measuring text. 94 // 95 // The length of this array is mTextLength. 96 private @Nullable char[] mCopiedBuffer; 97 98 // The whole paragraph direction. 99 private @Layout.Direction int mParaDir; 100 101 // True if the text is LTR direction and doesn't contain any bidi characters. 102 private boolean mLtrWithoutBidi; 103 104 // The bidi level for individual characters. 105 // 106 // This is empty if mLtrWithoutBidi is true. 107 private @NonNull ByteArray mLevels = new ByteArray(); 108 109 // The whole width of the text. 110 // See getWholeWidth comments. 111 private @FloatRange(from = 0.0f) float mWholeWidth; 112 113 // Individual characters' widths. 114 // See getWidths comments. 115 private @Nullable FloatArray mWidths = new FloatArray(); 116 117 // The span end positions. 118 // See getSpanEndCache comments. 119 private @Nullable IntArray mSpanEndCache = new IntArray(4); 120 121 // The font metrics. 122 // See getFontMetrics comments. 123 private @Nullable IntArray mFontMetrics = new IntArray(4 * 4); 124 125 // The native MeasuredParagraph. 126 private @Nullable MeasuredText mMeasuredText; 127 128 // Following three objects are for avoiding object allocation. 129 private @NonNull TextPaint mCachedPaint = new TextPaint(); 130 private @Nullable Paint.FontMetricsInt mCachedFm; 131 132 /** 133 * Releases internal buffers. 134 */ release()135 public void release() { 136 reset(); 137 mLevels.clearWithReleasingLargeArray(); 138 mWidths.clearWithReleasingLargeArray(); 139 mFontMetrics.clearWithReleasingLargeArray(); 140 mSpanEndCache.clearWithReleasingLargeArray(); 141 } 142 143 /** 144 * Resets the internal state for starting new text. 145 */ reset()146 private void reset() { 147 mSpanned = null; 148 mCopiedBuffer = null; 149 mWholeWidth = 0; 150 mLevels.clear(); 151 mWidths.clear(); 152 mFontMetrics.clear(); 153 mSpanEndCache.clear(); 154 mMeasuredText = null; 155 } 156 157 /** 158 * Returns the length of the paragraph. 159 * 160 * This is always available. 161 */ getTextLength()162 public int getTextLength() { 163 return mTextLength; 164 } 165 166 /** 167 * Returns the characters to be measured. 168 * 169 * This is always available. 170 */ getChars()171 public @NonNull char[] getChars() { 172 return mCopiedBuffer; 173 } 174 175 /** 176 * Returns the paragraph direction. 177 * 178 * This is always available. 179 */ getParagraphDir()180 public @Layout.Direction int getParagraphDir() { 181 return mParaDir; 182 } 183 184 /** 185 * Returns the directions. 186 * 187 * This is always available. 188 */ getDirections(@ntRangefrom = 0) int start, @IntRange(from = 0) int end)189 public Directions getDirections(@IntRange(from = 0) int start, // inclusive 190 @IntRange(from = 0) int end) { // exclusive 191 if (mLtrWithoutBidi) { 192 return Layout.DIRS_ALL_LEFT_TO_RIGHT; 193 } 194 195 final int length = end - start; 196 return AndroidBidi.directions(mParaDir, mLevels.getRawArray(), start, mCopiedBuffer, start, 197 length); 198 } 199 200 /** 201 * Returns the whole text width. 202 * 203 * This is available only if the MeasuredParagraph is computed with buildForMeasurement. 204 * Returns 0 in other cases. 205 */ getWholeWidth()206 public @FloatRange(from = 0.0f) float getWholeWidth() { 207 return mWholeWidth; 208 } 209 210 /** 211 * Returns the individual character's width. 212 * 213 * This is available only if the MeasuredParagraph is computed with buildForMeasurement. 214 * Returns empty array in other cases. 215 */ getWidths()216 public @NonNull FloatArray getWidths() { 217 return mWidths; 218 } 219 220 /** 221 * Returns the MetricsAffectingSpan end indices. 222 * 223 * If the input text is not a spanned string, this has one value that is the length of the text. 224 * 225 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout. 226 * Returns empty array in other cases. 227 */ getSpanEndCache()228 public @NonNull IntArray getSpanEndCache() { 229 return mSpanEndCache; 230 } 231 232 /** 233 * Returns the int array which holds FontMetrics. 234 * 235 * This array holds the repeat of top, bottom, ascent, descent of font metrics value. 236 * 237 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout. 238 * Returns empty array in other cases. 239 */ getFontMetrics()240 public @NonNull IntArray getFontMetrics() { 241 return mFontMetrics; 242 } 243 244 /** 245 * Returns the native ptr of the MeasuredParagraph. 246 * 247 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout. 248 * Returns null in other cases. 249 */ getMeasuredText()250 public MeasuredText getMeasuredText() { 251 return mMeasuredText; 252 } 253 254 /** 255 * Returns the width of the given range. 256 * 257 * This is not available if the MeasuredParagraph is computed with buildForBidi. 258 * Returns 0 if the MeasuredParagraph is computed with buildForBidi. 259 * 260 * @param start the inclusive start offset of the target region in the text 261 * @param end the exclusive end offset of the target region in the text 262 */ getWidth(int start, int end)263 public float getWidth(int start, int end) { 264 if (mMeasuredText == null) { 265 // We have result in Java. 266 final float[] widths = mWidths.getRawArray(); 267 float r = 0.0f; 268 for (int i = start; i < end; ++i) { 269 r += widths[i]; 270 } 271 return r; 272 } else { 273 // We have result in native. 274 return mMeasuredText.getWidth(start, end); 275 } 276 } 277 278 /** 279 * Retrieves the bounding rectangle that encloses all of the characters, with an implied origin 280 * at (0, 0). 281 * 282 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout. 283 */ getBounds(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull Rect bounds)284 public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end, 285 @NonNull Rect bounds) { 286 mMeasuredText.getBounds(start, end, bounds); 287 } 288 289 /** 290 * Retrieves the font metrics for the given range. 291 * 292 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout. 293 */ getFontMetricsInt(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull Paint.FontMetricsInt fmi)294 public void getFontMetricsInt(@IntRange(from = 0) int start, @IntRange(from = 0) int end, 295 @NonNull Paint.FontMetricsInt fmi) { 296 mMeasuredText.getFontMetricsInt(start, end, fmi); 297 } 298 299 /** 300 * Returns a width of the character at the offset. 301 * 302 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout. 303 */ getCharWidthAt(@ntRangefrom = 0) int offset)304 public float getCharWidthAt(@IntRange(from = 0) int offset) { 305 return mMeasuredText.getCharWidthAt(offset); 306 } 307 308 /** 309 * Generates new MeasuredParagraph for Bidi computation. 310 * 311 * If recycle is null, this returns new instance. If recycle is not null, this fills computed 312 * result to recycle and returns recycle. 313 * 314 * @param text the character sequence to be measured 315 * @param start the inclusive start offset of the target region in the text 316 * @param end the exclusive end offset of the target region in the text 317 * @param textDir the text direction 318 * @param recycle pass existing MeasuredParagraph if you want to recycle it. 319 * 320 * @return measured text 321 */ buildForBidi(@onNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir, @Nullable MeasuredParagraph recycle)322 public static @NonNull MeasuredParagraph buildForBidi(@NonNull CharSequence text, 323 @IntRange(from = 0) int start, 324 @IntRange(from = 0) int end, 325 @NonNull TextDirectionHeuristic textDir, 326 @Nullable MeasuredParagraph recycle) { 327 final MeasuredParagraph mt = recycle == null ? obtain() : recycle; 328 mt.resetAndAnalyzeBidi(text, start, end, textDir); 329 return mt; 330 } 331 332 /** 333 * Generates new MeasuredParagraph for measuring texts. 334 * 335 * If recycle is null, this returns new instance. If recycle is not null, this fills computed 336 * result to recycle and returns recycle. 337 * 338 * @param paint the paint to be used for rendering the text. 339 * @param text the character sequence to be measured 340 * @param start the inclusive start offset of the target region in the text 341 * @param end the exclusive end offset of the target region in the text 342 * @param textDir the text direction 343 * @param recycle pass existing MeasuredParagraph if you want to recycle it. 344 * 345 * @return measured text 346 */ buildForMeasurement(@onNull TextPaint paint, @NonNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir, @Nullable MeasuredParagraph recycle)347 public static @NonNull MeasuredParagraph buildForMeasurement(@NonNull TextPaint paint, 348 @NonNull CharSequence text, 349 @IntRange(from = 0) int start, 350 @IntRange(from = 0) int end, 351 @NonNull TextDirectionHeuristic textDir, 352 @Nullable MeasuredParagraph recycle) { 353 final MeasuredParagraph mt = recycle == null ? obtain() : recycle; 354 mt.resetAndAnalyzeBidi(text, start, end, textDir); 355 356 mt.mWidths.resize(mt.mTextLength); 357 if (mt.mTextLength == 0) { 358 return mt; 359 } 360 361 if (mt.mSpanned == null) { 362 // No style change by MetricsAffectingSpan. Just measure all text. 363 mt.applyMetricsAffectingSpan( 364 paint, null /* lineBreakConfig */, null /* spans */, start, end, 365 null /* native builder ptr */); 366 } else { 367 // There may be a MetricsAffectingSpan. Split into span transitions and apply styles. 368 int spanEnd; 369 for (int spanStart = start; spanStart < end; spanStart = spanEnd) { 370 spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class); 371 MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd, 372 MetricAffectingSpan.class); 373 spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class); 374 mt.applyMetricsAffectingSpan( 375 paint, null /* line break config */, spans, spanStart, spanEnd, 376 null /* native builder ptr */); 377 } 378 } 379 return mt; 380 } 381 382 /** 383 * Generates new MeasuredParagraph for StaticLayout. 384 * 385 * If recycle is null, this returns new instance. If recycle is not null, this fills computed 386 * result to recycle and returns recycle. 387 * 388 * @param paint the paint to be used for rendering the text. 389 * @param lineBreakConfig the line break configuration for text wrapping. 390 * @param text the character sequence to be measured 391 * @param start the inclusive start offset of the target region in the text 392 * @param end the exclusive end offset of the target region in the text 393 * @param textDir the text direction 394 * @param hyphenationMode a hyphenation mode 395 * @param computeLayout true if need to compute full layout, otherwise false. 396 * @param hint pass if you already have measured paragraph. 397 * @param recycle pass existing MeasuredParagraph if you want to recycle it. 398 * 399 * @return measured text 400 */ buildForStaticLayout( @onNull TextPaint paint, @Nullable LineBreakConfig lineBreakConfig, @NonNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir, int hyphenationMode, boolean computeLayout, @Nullable MeasuredParagraph hint, @Nullable MeasuredParagraph recycle)401 public static @NonNull MeasuredParagraph buildForStaticLayout( 402 @NonNull TextPaint paint, 403 @Nullable LineBreakConfig lineBreakConfig, 404 @NonNull CharSequence text, 405 @IntRange(from = 0) int start, 406 @IntRange(from = 0) int end, 407 @NonNull TextDirectionHeuristic textDir, 408 int hyphenationMode, 409 boolean computeLayout, 410 @Nullable MeasuredParagraph hint, 411 @Nullable MeasuredParagraph recycle) { 412 final MeasuredParagraph mt = recycle == null ? obtain() : recycle; 413 mt.resetAndAnalyzeBidi(text, start, end, textDir); 414 final MeasuredText.Builder builder; 415 if (hint == null) { 416 builder = new MeasuredText.Builder(mt.mCopiedBuffer) 417 .setComputeHyphenation(hyphenationMode) 418 .setComputeLayout(computeLayout); 419 } else { 420 builder = new MeasuredText.Builder(hint.mMeasuredText); 421 } 422 if (mt.mTextLength == 0) { 423 // Need to build empty native measured text for StaticLayout. 424 // TODO: Stop creating empty measured text for empty lines. 425 mt.mMeasuredText = builder.build(); 426 } else { 427 if (mt.mSpanned == null) { 428 // No style change by MetricsAffectingSpan. Just measure all text. 429 mt.applyMetricsAffectingSpan(paint, lineBreakConfig, null /* spans */, start, end, 430 builder); 431 mt.mSpanEndCache.append(end); 432 } else { 433 // There may be a MetricsAffectingSpan. Split into span transitions and apply 434 // styles. 435 int spanEnd; 436 for (int spanStart = start; spanStart < end; spanStart = spanEnd) { 437 spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, 438 MetricAffectingSpan.class); 439 MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd, 440 MetricAffectingSpan.class); 441 spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, 442 MetricAffectingSpan.class); 443 // TODO: Update line break config with spans. 444 mt.applyMetricsAffectingSpan(paint, lineBreakConfig, spans, spanStart, spanEnd, 445 builder); 446 mt.mSpanEndCache.append(spanEnd); 447 } 448 } 449 mt.mMeasuredText = builder.build(); 450 } 451 452 return mt; 453 } 454 455 /** 456 * Reset internal state and analyzes text for bidirectional runs. 457 * 458 * @param text the character sequence to be measured 459 * @param start the inclusive start offset of the target region in the text 460 * @param end the exclusive end offset of the target region in the text 461 * @param textDir the text direction 462 */ resetAndAnalyzeBidi(@onNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir)463 private void resetAndAnalyzeBidi(@NonNull CharSequence text, 464 @IntRange(from = 0) int start, // inclusive 465 @IntRange(from = 0) int end, // exclusive 466 @NonNull TextDirectionHeuristic textDir) { 467 reset(); 468 mSpanned = text instanceof Spanned ? (Spanned) text : null; 469 mTextStart = start; 470 mTextLength = end - start; 471 472 if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) { 473 mCopiedBuffer = new char[mTextLength]; 474 } 475 TextUtils.getChars(text, start, end, mCopiedBuffer, 0); 476 477 // Replace characters associated with ReplacementSpan to U+FFFC. 478 if (mSpanned != null) { 479 ReplacementSpan[] spans = mSpanned.getSpans(start, end, ReplacementSpan.class); 480 481 for (int i = 0; i < spans.length; i++) { 482 int startInPara = mSpanned.getSpanStart(spans[i]) - start; 483 int endInPara = mSpanned.getSpanEnd(spans[i]) - start; 484 // The span interval may be larger and must be restricted to [start, end) 485 if (startInPara < 0) startInPara = 0; 486 if (endInPara > mTextLength) endInPara = mTextLength; 487 Arrays.fill(mCopiedBuffer, startInPara, endInPara, OBJECT_REPLACEMENT_CHARACTER); 488 } 489 } 490 491 if ((textDir == TextDirectionHeuristics.LTR 492 || textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR 493 || textDir == TextDirectionHeuristics.ANYRTL_LTR) 494 && TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) { 495 mLevels.clear(); 496 mParaDir = Layout.DIR_LEFT_TO_RIGHT; 497 mLtrWithoutBidi = true; 498 } else { 499 final int bidiRequest; 500 if (textDir == TextDirectionHeuristics.LTR) { 501 bidiRequest = Layout.DIR_REQUEST_LTR; 502 } else if (textDir == TextDirectionHeuristics.RTL) { 503 bidiRequest = Layout.DIR_REQUEST_RTL; 504 } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) { 505 bidiRequest = Layout.DIR_REQUEST_DEFAULT_LTR; 506 } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) { 507 bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL; 508 } else { 509 final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength); 510 bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR; 511 } 512 mLevels.resize(mTextLength); 513 mParaDir = AndroidBidi.bidi(bidiRequest, mCopiedBuffer, mLevels.getRawArray()); 514 mLtrWithoutBidi = false; 515 } 516 } 517 applyReplacementRun(@onNull ReplacementSpan replacement, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextPaint paint, @Nullable MeasuredText.Builder builder)518 private void applyReplacementRun(@NonNull ReplacementSpan replacement, 519 @IntRange(from = 0) int start, // inclusive, in copied buffer 520 @IntRange(from = 0) int end, // exclusive, in copied buffer 521 @NonNull TextPaint paint, 522 @Nullable MeasuredText.Builder builder) { 523 // Use original text. Shouldn't matter. 524 // TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for 525 // backward compatibility? or Should we initialize them for getFontMetricsInt? 526 final float width = replacement.getSize( 527 paint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm); 528 if (builder == null) { 529 // Assigns all width to the first character. This is the same behavior as minikin. 530 mWidths.set(start, width); 531 if (end > start + 1) { 532 Arrays.fill(mWidths.getRawArray(), start + 1, end, 0.0f); 533 } 534 mWholeWidth += width; 535 } else { 536 builder.appendReplacementRun(paint, end - start, width); 537 } 538 } 539 applyStyleRun(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull TextPaint paint, @Nullable LineBreakConfig config, @Nullable MeasuredText.Builder builder)540 private void applyStyleRun(@IntRange(from = 0) int start, // inclusive, in copied buffer 541 @IntRange(from = 0) int end, // exclusive, in copied buffer 542 @NonNull TextPaint paint, 543 @Nullable LineBreakConfig config, 544 @Nullable MeasuredText.Builder builder) { 545 546 if (mLtrWithoutBidi) { 547 // If the whole text is LTR direction, just apply whole region. 548 if (builder == null) { 549 mWholeWidth += paint.getTextRunAdvances( 550 mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */, 551 mWidths.getRawArray(), start); 552 } else { 553 builder.appendStyleRun(paint, config, end - start, false /* isRtl */); 554 } 555 } else { 556 // If there is multiple bidi levels, split into individual bidi level and apply style. 557 byte level = mLevels.get(start); 558 // Note that the empty text or empty range won't reach this method. 559 // Safe to search from start + 1. 560 for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) { 561 if (levelEnd == end || mLevels.get(levelEnd) != level) { // transition point 562 final boolean isRtl = (level & 0x1) != 0; 563 if (builder == null) { 564 final int levelLength = levelEnd - levelStart; 565 mWholeWidth += paint.getTextRunAdvances( 566 mCopiedBuffer, levelStart, levelLength, levelStart, levelLength, 567 isRtl, mWidths.getRawArray(), levelStart); 568 } else { 569 builder.appendStyleRun(paint, config, levelEnd - levelStart, isRtl); 570 } 571 if (levelEnd == end) { 572 break; 573 } 574 levelStart = levelEnd; 575 level = mLevels.get(levelEnd); 576 } 577 } 578 } 579 } 580 applyMetricsAffectingSpan( @onNull TextPaint paint, @Nullable LineBreakConfig lineBreakConfig, @Nullable MetricAffectingSpan[] spans, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @Nullable MeasuredText.Builder builder)581 private void applyMetricsAffectingSpan( 582 @NonNull TextPaint paint, 583 @Nullable LineBreakConfig lineBreakConfig, 584 @Nullable MetricAffectingSpan[] spans, 585 @IntRange(from = 0) int start, // inclusive, in original text buffer 586 @IntRange(from = 0) int end, // exclusive, in original text buffer 587 @Nullable MeasuredText.Builder builder) { 588 mCachedPaint.set(paint); 589 // XXX paint should not have a baseline shift, but... 590 mCachedPaint.baselineShift = 0; 591 592 final boolean needFontMetrics = builder != null; 593 594 if (needFontMetrics && mCachedFm == null) { 595 mCachedFm = new Paint.FontMetricsInt(); 596 } 597 598 ReplacementSpan replacement = null; 599 if (spans != null) { 600 for (int i = 0; i < spans.length; i++) { 601 MetricAffectingSpan span = spans[i]; 602 if (span instanceof ReplacementSpan) { 603 // The last ReplacementSpan is effective for backward compatibility reasons. 604 replacement = (ReplacementSpan) span; 605 } else { 606 // TODO: No need to call updateMeasureState for ReplacementSpan as well? 607 span.updateMeasureState(mCachedPaint); 608 } 609 } 610 } 611 612 final int startInCopiedBuffer = start - mTextStart; 613 final int endInCopiedBuffer = end - mTextStart; 614 615 if (builder != null) { 616 mCachedPaint.getFontMetricsInt(mCachedFm); 617 } 618 619 if (replacement != null) { 620 applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer, mCachedPaint, 621 builder); 622 } else { 623 applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, mCachedPaint, 624 lineBreakConfig, builder); 625 } 626 627 if (needFontMetrics) { 628 if (mCachedPaint.baselineShift < 0) { 629 mCachedFm.ascent += mCachedPaint.baselineShift; 630 mCachedFm.top += mCachedPaint.baselineShift; 631 } else { 632 mCachedFm.descent += mCachedPaint.baselineShift; 633 mCachedFm.bottom += mCachedPaint.baselineShift; 634 } 635 636 mFontMetrics.append(mCachedFm.top); 637 mFontMetrics.append(mCachedFm.bottom); 638 mFontMetrics.append(mCachedFm.ascent); 639 mFontMetrics.append(mCachedFm.descent); 640 } 641 } 642 643 /** 644 * Returns the maximum index that the accumulated width not exceeds the width. 645 * 646 * If forward=false is passed, returns the minimum index from the end instead. 647 * 648 * This only works if the MeasuredParagraph is computed with buildForMeasurement. 649 * Undefined behavior in other case. 650 */ breakText(int limit, boolean forwards, float width)651 @IntRange(from = 0) int breakText(int limit, boolean forwards, float width) { 652 float[] w = mWidths.getRawArray(); 653 if (forwards) { 654 int i = 0; 655 while (i < limit) { 656 width -= w[i]; 657 if (width < 0.0f) break; 658 i++; 659 } 660 while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--; 661 return i; 662 } else { 663 int i = limit - 1; 664 while (i >= 0) { 665 width -= w[i]; 666 if (width < 0.0f) break; 667 i--; 668 } 669 while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) { 670 i++; 671 } 672 return limit - i - 1; 673 } 674 } 675 676 /** 677 * Returns the length of the substring. 678 * 679 * This only works if the MeasuredParagraph is computed with buildForMeasurement. 680 * Undefined behavior in other case. 681 */ measure(int start, int limit)682 @FloatRange(from = 0.0f) float measure(int start, int limit) { 683 float width = 0; 684 float[] w = mWidths.getRawArray(); 685 for (int i = start; i < limit; ++i) { 686 width += w[i]; 687 } 688 return width; 689 } 690 691 /** 692 * This only works if the MeasuredParagraph is computed with buildForStaticLayout. 693 */ getMemoryUsage()694 public @IntRange(from = 0) int getMemoryUsage() { 695 return mMeasuredText.getMemoryUsage(); 696 } 697 } 698