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