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