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