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 static com.android.text.flags.Flags.FLAG_NO_BREAK_NO_HYPHENATION_SPAN; 20 21 import android.annotation.FlaggedApi; 22 import android.annotation.FloatRange; 23 import android.annotation.IntRange; 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.annotation.Px; 27 import android.annotation.SuppressLint; 28 import android.annotation.TestApi; 29 import android.graphics.Paint; 30 import android.graphics.Rect; 31 import android.graphics.text.LineBreakConfig; 32 import android.graphics.text.MeasuredText; 33 import android.icu.lang.UCharacter; 34 import android.icu.lang.UCharacterDirection; 35 import android.icu.text.Bidi; 36 import android.text.AutoGrowArray.ByteArray; 37 import android.text.AutoGrowArray.FloatArray; 38 import android.text.AutoGrowArray.IntArray; 39 import android.text.Layout.Directions; 40 import android.text.style.LineBreakConfigSpan; 41 import android.text.style.MetricAffectingSpan; 42 import android.text.style.ReplacementSpan; 43 import android.util.Pools.SynchronizedPool; 44 45 import java.util.Arrays; 46 47 /** 48 * MeasuredParagraph provides text information for rendering purpose. 49 * 50 * The first motivation of this class is identify the text directions and retrieving individual 51 * character widths. However retrieving character widths is slower than identifying text directions. 52 * Thus, this class provides several builder methods for specific purposes. 53 * 54 * - buildForBidi: 55 * Compute only text directions. 56 * - buildForMeasurement: 57 * Compute text direction and all character widths. 58 * - buildForStaticLayout: 59 * This is bit special. StaticLayout also needs to know text direction and character widths for 60 * line breaking, but all things are done in native code. Similarly, text measurement is done 61 * in native code. So instead of storing result to Java array, this keeps the result in native 62 * code since there is no good reason to move the results to Java layer. 63 * 64 * In addition to the character widths, some additional information is computed for each purposes, 65 * e.g. whole text length for measurement or font metrics for static layout. 66 * 67 * MeasuredParagraph is NOT a thread safe object. 68 * @hide 69 */ 70 @TestApi 71 public class MeasuredParagraph { 72 private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC'; 73 MeasuredParagraph()74 private MeasuredParagraph() {} // Use build static functions instead. 75 76 private static final SynchronizedPool<MeasuredParagraph> sPool = new SynchronizedPool<>(1); 77 obtain()78 private static @NonNull MeasuredParagraph obtain() { // Use build static functions instead. 79 final MeasuredParagraph mt = sPool.acquire(); 80 return mt != null ? mt : new MeasuredParagraph(); 81 } 82 83 /** 84 * Recycle the MeasuredParagraph. 85 * 86 * Do not call any methods after you call this method. 87 * @hide 88 */ recycle()89 public void recycle() { 90 release(); 91 sPool.release(this); 92 } 93 94 // The casted original text. 95 // 96 // This may be null if the passed text is not a Spanned. 97 private @Nullable Spanned mSpanned; 98 99 // The start offset of the target range in the original text (mSpanned); 100 private @IntRange(from = 0) int mTextStart; 101 102 // The length of the target range in the original text. 103 private @IntRange(from = 0) int mTextLength; 104 105 // The copied character buffer for measuring text. 106 // 107 // The length of this array is mTextLength. 108 private @Nullable char[] mCopiedBuffer; 109 110 // The whole paragraph direction. 111 private @Layout.Direction int mParaDir; 112 113 // True if the text is LTR direction and doesn't contain any bidi characters. 114 private boolean mLtrWithoutBidi; 115 116 // The bidi level for individual characters. 117 // 118 // This is empty if mLtrWithoutBidi is true. 119 private @NonNull ByteArray mLevels = new ByteArray(); 120 121 private Bidi mBidi; 122 123 // The whole width of the text. 124 // See getWholeWidth comments. 125 private @FloatRange(from = 0.0f) float mWholeWidth; 126 127 // Individual characters' widths. 128 // See getWidths comments. 129 private @Nullable FloatArray mWidths = new FloatArray(); 130 131 // The span end positions. 132 // See getSpanEndCache comments. 133 private @Nullable IntArray mSpanEndCache = new IntArray(4); 134 135 // The font metrics. 136 // See getFontMetrics comments. 137 private @Nullable IntArray mFontMetrics = new IntArray(4 * 4); 138 139 // The native MeasuredParagraph. 140 private @Nullable MeasuredText mMeasuredText; 141 142 // Following three objects are for avoiding object allocation. 143 private final @NonNull TextPaint mCachedPaint = new TextPaint(); 144 private @Nullable Paint.FontMetricsInt mCachedFm; 145 private final @NonNull LineBreakConfig.Builder mLineBreakConfigBuilder = 146 new LineBreakConfig.Builder(); 147 148 /** 149 * Releases internal buffers. 150 * @hide 151 */ release()152 public void release() { 153 reset(); 154 mLevels.clearWithReleasingLargeArray(); 155 mWidths.clearWithReleasingLargeArray(); 156 mFontMetrics.clearWithReleasingLargeArray(); 157 mSpanEndCache.clearWithReleasingLargeArray(); 158 } 159 160 /** 161 * Resets the internal state for starting new text. 162 */ reset()163 private void reset() { 164 mSpanned = null; 165 mCopiedBuffer = null; 166 mWholeWidth = 0; 167 mLevels.clear(); 168 mWidths.clear(); 169 mFontMetrics.clear(); 170 mSpanEndCache.clear(); 171 mMeasuredText = null; 172 mBidi = null; 173 } 174 175 /** 176 * Returns the length of the paragraph. 177 * 178 * This is always available. 179 * @hide 180 */ getTextLength()181 public int getTextLength() { 182 return mTextLength; 183 } 184 185 /** 186 * Returns the characters to be measured. 187 * 188 * This is always available. 189 * @hide 190 */ getChars()191 public @NonNull char[] getChars() { 192 return mCopiedBuffer; 193 } 194 195 /** 196 * Returns the paragraph direction. 197 * 198 * This is always available. 199 * @hide 200 */ getParagraphDir()201 public @Layout.Direction int getParagraphDir() { 202 if (ClientFlags.icuBidiMigration()) { 203 if (mBidi == null) { 204 return Layout.DIR_LEFT_TO_RIGHT; 205 } 206 return (mBidi.getParaLevel() & 0x01) == 0 207 ? Layout.DIR_LEFT_TO_RIGHT : Layout.DIR_RIGHT_TO_LEFT; 208 } 209 return mParaDir; 210 } 211 212 /** 213 * Returns the directions. 214 * 215 * This is always available. 216 * @hide 217 */ getDirections(@ntRangefrom = 0) int start, @IntRange(from = 0) int end)218 public Directions getDirections(@IntRange(from = 0) int start, // inclusive 219 @IntRange(from = 0) int end) { // exclusive 220 if (ClientFlags.icuBidiMigration()) { 221 // Easy case: mBidi == null means the text is all LTR and no bidi suppot is needed. 222 if (mBidi == null) { 223 return Layout.DIRS_ALL_LEFT_TO_RIGHT; 224 } 225 226 // Easy case: If the original text only contains single directionality run, the 227 // substring is only single run. 228 if (start == end) { 229 if ((mBidi.getParaLevel() & 0x01) == 0) { 230 return Layout.DIRS_ALL_LEFT_TO_RIGHT; 231 } else { 232 return Layout.DIRS_ALL_RIGHT_TO_LEFT; 233 } 234 } 235 236 // Okay, now we need to generate the line instance. 237 Bidi bidi = mBidi.createLineBidi(start, end); 238 239 // Easy case: If the line instance only contains single directionality run, no need 240 // to reorder visually. 241 if (bidi.getRunCount() == 1) { 242 if (bidi.getRunLevel(0) == 1) { 243 return Layout.DIRS_ALL_RIGHT_TO_LEFT; 244 } else if (bidi.getRunLevel(0) == 0) { 245 return Layout.DIRS_ALL_LEFT_TO_RIGHT; 246 } else { 247 return new Directions(new int[] { 248 0, bidi.getRunLevel(0) << Layout.RUN_LEVEL_SHIFT | (end - start)}); 249 } 250 } 251 252 // Reorder directionality run visually. 253 byte[] levels = new byte[bidi.getRunCount()]; 254 for (int i = 0; i < bidi.getRunCount(); ++i) { 255 levels[i] = (byte) bidi.getRunLevel(i); 256 } 257 int[] visualOrders = Bidi.reorderVisual(levels); 258 259 int[] dirs = new int[bidi.getRunCount() * 2]; 260 for (int i = 0; i < bidi.getRunCount(); ++i) { 261 int vIndex; 262 if ((mBidi.getBaseLevel() & 0x01) == 1) { 263 // For the historical reasons, if the base directionality is RTL, the Android 264 // draws from the right, i.e. the visually reordered run needs to be reversed. 265 vIndex = visualOrders[bidi.getRunCount() - i - 1]; 266 } else { 267 vIndex = visualOrders[i]; 268 } 269 270 // Special packing of dire 271 dirs[i * 2] = bidi.getRunStart(vIndex); 272 dirs[i * 2 + 1] = bidi.getRunLevel(vIndex) << Layout.RUN_LEVEL_SHIFT 273 | (bidi.getRunLimit(vIndex) - dirs[i * 2]); 274 } 275 276 return new Directions(dirs); 277 } 278 if (mLtrWithoutBidi) { 279 return Layout.DIRS_ALL_LEFT_TO_RIGHT; 280 } 281 282 final int length = end - start; 283 return AndroidBidi.directions(mParaDir, mLevels.getRawArray(), start, mCopiedBuffer, start, 284 length); 285 } 286 287 /** 288 * Returns the whole text width. 289 * 290 * This is available only if the MeasuredParagraph is computed with buildForMeasurement. 291 * Returns 0 in other cases. 292 * @hide 293 */ getWholeWidth()294 public @FloatRange(from = 0.0f) float getWholeWidth() { 295 return mWholeWidth; 296 } 297 298 /** 299 * Returns the individual character's width. 300 * 301 * This is available only if the MeasuredParagraph is computed with buildForMeasurement. 302 * Returns empty array in other cases. 303 * @hide 304 */ getWidths()305 public @NonNull FloatArray getWidths() { 306 return mWidths; 307 } 308 309 /** 310 * Returns the MetricsAffectingSpan end indices. 311 * 312 * If the input text is not a spanned string, this has one value that is the length of the text. 313 * 314 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout. 315 * Returns empty array in other cases. 316 * @hide 317 */ getSpanEndCache()318 public @NonNull IntArray getSpanEndCache() { 319 return mSpanEndCache; 320 } 321 322 /** 323 * Returns the int array which holds FontMetrics. 324 * 325 * This array holds the repeat of top, bottom, ascent, descent of font metrics value. 326 * 327 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout. 328 * Returns empty array in other cases. 329 * @hide 330 */ getFontMetrics()331 public @NonNull IntArray getFontMetrics() { 332 return mFontMetrics; 333 } 334 335 /** 336 * Returns the native ptr of the MeasuredParagraph. 337 * 338 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout. 339 * Returns null in other cases. 340 * @hide 341 */ getMeasuredText()342 public MeasuredText getMeasuredText() { 343 return mMeasuredText; 344 } 345 346 /** 347 * Returns the width of the given range. 348 * 349 * This is not available if the MeasuredParagraph is computed with buildForBidi. 350 * Returns 0 if the MeasuredParagraph is computed with buildForBidi. 351 * 352 * @param start the inclusive start offset of the target region in the text 353 * @param end the exclusive end offset of the target region in the text 354 * @hide 355 */ getWidth(int start, int end)356 public float getWidth(int start, int end) { 357 if (mMeasuredText == null) { 358 // We have result in Java. 359 final float[] widths = mWidths.getRawArray(); 360 float r = 0.0f; 361 for (int i = start; i < end; ++i) { 362 r += widths[i]; 363 } 364 return r; 365 } else { 366 // We have result in native. 367 return mMeasuredText.getWidth(start, end); 368 } 369 } 370 371 /** 372 * Retrieves the bounding rectangle that encloses all of the characters, with an implied origin 373 * at (0, 0). 374 * 375 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout. 376 * @hide 377 */ getBounds(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull Rect bounds)378 public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end, 379 @NonNull Rect bounds) { 380 mMeasuredText.getBounds(start, end, bounds); 381 } 382 383 /** 384 * Retrieves the font metrics for the given range. 385 * 386 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout. 387 * @hide 388 */ getFontMetricsInt(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull Paint.FontMetricsInt fmi)389 public void getFontMetricsInt(@IntRange(from = 0) int start, @IntRange(from = 0) int end, 390 @NonNull Paint.FontMetricsInt fmi) { 391 mMeasuredText.getFontMetricsInt(start, end, fmi); 392 } 393 394 /** 395 * Returns a width of the character at the offset. 396 * 397 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout. 398 * @hide 399 */ getCharWidthAt(@ntRangefrom = 0) int offset)400 public float getCharWidthAt(@IntRange(from = 0) int offset) { 401 return mMeasuredText.getCharWidthAt(offset); 402 } 403 404 /** 405 * Generates new MeasuredParagraph for Bidi computation. 406 * 407 * If recycle is null, this returns new instance. If recycle is not null, this fills computed 408 * result to recycle and returns recycle. 409 * 410 * @param text the character sequence to be measured 411 * @param start the inclusive start offset of the target region in the text 412 * @param end the exclusive end offset of the target region in the text 413 * @param textDir the text direction 414 * @param recycle pass existing MeasuredParagraph if you want to recycle it. 415 * 416 * @return measured text 417 * @hide 418 */ buildForBidi(@onNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir, @Nullable MeasuredParagraph recycle)419 public static @NonNull MeasuredParagraph buildForBidi(@NonNull CharSequence text, 420 @IntRange(from = 0) int start, 421 @IntRange(from = 0) int end, 422 @NonNull TextDirectionHeuristic textDir, 423 @Nullable MeasuredParagraph recycle) { 424 final MeasuredParagraph mt = recycle == null ? obtain() : recycle; 425 mt.resetAndAnalyzeBidi(text, start, end, textDir); 426 return mt; 427 } 428 429 /** 430 * Generates new MeasuredParagraph for measuring texts. 431 * 432 * If recycle is null, this returns new instance. If recycle is not null, this fills computed 433 * result to recycle and returns recycle. 434 * 435 * @param paint the paint to be used for rendering the text. 436 * @param text the character sequence to be measured 437 * @param start the inclusive start offset of the target region in the text 438 * @param end the exclusive end offset of the target region in the text 439 * @param textDir the text direction 440 * @param recycle pass existing MeasuredParagraph if you want to recycle it. 441 * 442 * @return measured text 443 * @hide 444 */ buildForMeasurement(@onNull TextPaint paint, @NonNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir, @Nullable MeasuredParagraph recycle)445 public static @NonNull MeasuredParagraph buildForMeasurement(@NonNull TextPaint paint, 446 @NonNull CharSequence text, 447 @IntRange(from = 0) int start, 448 @IntRange(from = 0) int end, 449 @NonNull TextDirectionHeuristic textDir, 450 @Nullable MeasuredParagraph recycle) { 451 final MeasuredParagraph mt = recycle == null ? obtain() : recycle; 452 mt.resetAndAnalyzeBidi(text, start, end, textDir); 453 454 mt.mWidths.resize(mt.mTextLength); 455 if (mt.mTextLength == 0) { 456 return mt; 457 } 458 459 if (mt.mSpanned == null) { 460 // No style change by MetricsAffectingSpan. Just measure all text. 461 mt.applyMetricsAffectingSpan( 462 paint, null /* lineBreakConfig */, null /* spans */, null /* lbcSpans */, 463 start, end, null /* native builder ptr */, null); 464 } else { 465 // There may be a MetricsAffectingSpan. Split into span transitions and apply styles. 466 int spanEnd; 467 for (int spanStart = start; spanStart < end; spanStart = spanEnd) { 468 int maSpanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, 469 MetricAffectingSpan.class); 470 int lbcSpanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, 471 LineBreakConfigSpan.class); 472 spanEnd = Math.min(maSpanEnd, lbcSpanEnd); 473 MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd, 474 MetricAffectingSpan.class); 475 LineBreakConfigSpan[] lbcSpans = mt.mSpanned.getSpans(spanStart, spanEnd, 476 LineBreakConfigSpan.class); 477 spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class); 478 lbcSpans = TextUtils.removeEmptySpans(lbcSpans, mt.mSpanned, 479 LineBreakConfigSpan.class); 480 mt.applyMetricsAffectingSpan( 481 paint, null /* line break config */, spans, lbcSpans, spanStart, spanEnd, 482 null /* native builder ptr */, null); 483 } 484 } 485 return mt; 486 } 487 488 /** 489 * A test interface for observing the style run calculation. 490 * @hide 491 */ 492 @TestApi 493 @FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN) 494 public interface StyleRunCallback { 495 /** 496 * Called when a single style run is identified. 497 */ 498 @FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN) onAppendStyleRun(@onNull Paint paint, @Nullable LineBreakConfig lineBreakConfig, @IntRange(from = 0) int length, boolean isRtl)499 void onAppendStyleRun(@NonNull Paint paint, 500 @Nullable LineBreakConfig lineBreakConfig, @IntRange(from = 0) int length, 501 boolean isRtl); 502 503 /** 504 * Called when a single replacement run is identified. 505 */ 506 @FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN) onAppendReplacementRun(@onNull Paint paint, @IntRange(from = 0) int length, @Px @FloatRange(from = 0) float width)507 void onAppendReplacementRun(@NonNull Paint paint, 508 @IntRange(from = 0) int length, @Px @FloatRange(from = 0) float width); 509 } 510 511 /** 512 * Generates new MeasuredParagraph for StaticLayout. 513 * 514 * If recycle is null, this returns new instance. If recycle is not null, this fills computed 515 * result to recycle and returns recycle. 516 * 517 * @param paint the paint to be used for rendering the text. 518 * @param lineBreakConfig the line break configuration for text wrapping. 519 * @param text the character sequence to be measured 520 * @param start the inclusive start offset of the target region in the text 521 * @param end the exclusive end offset of the target region in the text 522 * @param textDir the text direction 523 * @param hyphenationMode a hyphenation mode 524 * @param computeLayout true if need to compute full layout, otherwise false. 525 * @param hint pass if you already have measured paragraph. 526 * @param recycle pass existing MeasuredParagraph if you want to recycle it. 527 * 528 * @return measured text 529 * @hide 530 */ 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, boolean computeBounds, @Nullable MeasuredParagraph hint, @Nullable MeasuredParagraph recycle)531 public static @NonNull MeasuredParagraph buildForStaticLayout( 532 @NonNull TextPaint paint, 533 @Nullable LineBreakConfig lineBreakConfig, 534 @NonNull CharSequence text, 535 @IntRange(from = 0) int start, 536 @IntRange(from = 0) int end, 537 @NonNull TextDirectionHeuristic textDir, 538 int hyphenationMode, 539 boolean computeLayout, 540 boolean computeBounds, 541 @Nullable MeasuredParagraph hint, 542 @Nullable MeasuredParagraph recycle) { 543 return buildForStaticLayoutInternal(paint, lineBreakConfig, text, start, end, textDir, 544 hyphenationMode, computeLayout, computeBounds, hint, recycle, null); 545 } 546 547 /** 548 * Generates new MeasuredParagraph for StaticLayout. 549 * 550 * If recycle is null, this returns new instance. If recycle is not null, this fills computed 551 * result to recycle and returns recycle. 552 * 553 * @param paint the paint to be used for rendering the text. 554 * @param lineBreakConfig the line break configuration for text wrapping. 555 * @param text the character sequence to be measured 556 * @param start the inclusive start offset of the target region in the text 557 * @param end the exclusive end offset of the target region in the text 558 * @param textDir the text direction 559 * @param hyphenationMode a hyphenation mode 560 * @param computeLayout true if need to compute full layout, otherwise false. 561 * 562 * @return measured text 563 * @hide 564 */ 565 @SuppressLint("ExecutorRegistration") 566 @TestApi 567 @NonNull 568 @FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN) buildForStaticLayoutTest( @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 StyleRunCallback testCallback)569 public static MeasuredParagraph buildForStaticLayoutTest( 570 @NonNull TextPaint paint, 571 @Nullable LineBreakConfig lineBreakConfig, 572 @NonNull CharSequence text, 573 @IntRange(from = 0) int start, 574 @IntRange(from = 0) int end, 575 @NonNull TextDirectionHeuristic textDir, 576 int hyphenationMode, 577 boolean computeLayout, 578 @Nullable StyleRunCallback testCallback) { 579 return buildForStaticLayoutInternal(paint, lineBreakConfig, text, start, end, textDir, 580 hyphenationMode, computeLayout, false, null, null, testCallback); 581 } 582 buildForStaticLayoutInternal( @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, boolean computeBounds, @Nullable MeasuredParagraph hint, @Nullable MeasuredParagraph recycle, @Nullable StyleRunCallback testCallback)583 private static @NonNull MeasuredParagraph buildForStaticLayoutInternal( 584 @NonNull TextPaint paint, 585 @Nullable LineBreakConfig lineBreakConfig, 586 @NonNull CharSequence text, 587 @IntRange(from = 0) int start, 588 @IntRange(from = 0) int end, 589 @NonNull TextDirectionHeuristic textDir, 590 int hyphenationMode, 591 boolean computeLayout, 592 boolean computeBounds, 593 @Nullable MeasuredParagraph hint, 594 @Nullable MeasuredParagraph recycle, 595 @Nullable StyleRunCallback testCallback) { 596 final MeasuredParagraph mt = recycle == null ? obtain() : recycle; 597 mt.resetAndAnalyzeBidi(text, start, end, textDir); 598 final MeasuredText.Builder builder; 599 if (hint == null) { 600 builder = new MeasuredText.Builder(mt.mCopiedBuffer) 601 .setComputeHyphenation(hyphenationMode) 602 .setComputeLayout(computeLayout) 603 .setComputeBounds(computeBounds); 604 } else { 605 builder = new MeasuredText.Builder(hint.mMeasuredText); 606 } 607 if (mt.mTextLength == 0) { 608 // Need to build empty native measured text for StaticLayout. 609 // TODO: Stop creating empty measured text for empty lines. 610 mt.mMeasuredText = builder.build(); 611 } else { 612 if (mt.mSpanned == null) { 613 // No style change by MetricsAffectingSpan. Just measure all text. 614 mt.applyMetricsAffectingSpan(paint, lineBreakConfig, null /* spans */, null, 615 start, end, builder, testCallback); 616 mt.mSpanEndCache.append(end); 617 } else { 618 // There may be a MetricsAffectingSpan. Split into span transitions and apply 619 // styles. 620 int spanEnd; 621 for (int spanStart = start; spanStart < end; spanStart = spanEnd) { 622 int maSpanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, 623 MetricAffectingSpan.class); 624 int lbcSpanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, 625 LineBreakConfigSpan.class); 626 spanEnd = Math.min(maSpanEnd, lbcSpanEnd); 627 MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd, 628 MetricAffectingSpan.class); 629 LineBreakConfigSpan[] lbcSpans = mt.mSpanned.getSpans(spanStart, spanEnd, 630 LineBreakConfigSpan.class); 631 spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, 632 MetricAffectingSpan.class); 633 lbcSpans = TextUtils.removeEmptySpans(lbcSpans, mt.mSpanned, 634 LineBreakConfigSpan.class); 635 mt.applyMetricsAffectingSpan(paint, lineBreakConfig, spans, lbcSpans, spanStart, 636 spanEnd, builder, testCallback); 637 mt.mSpanEndCache.append(spanEnd); 638 } 639 } 640 mt.mMeasuredText = builder.build(); 641 } 642 643 return mt; 644 } 645 646 /** 647 * Reset internal state and analyzes text for bidirectional runs. 648 * 649 * @param text the character sequence to be measured 650 * @param start the inclusive start offset of the target region in the text 651 * @param end the exclusive end offset of the target region in the text 652 * @param textDir the text direction 653 */ resetAndAnalyzeBidi(@onNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir)654 private void resetAndAnalyzeBidi(@NonNull CharSequence text, 655 @IntRange(from = 0) int start, // inclusive 656 @IntRange(from = 0) int end, // exclusive 657 @NonNull TextDirectionHeuristic textDir) { 658 reset(); 659 mSpanned = text instanceof Spanned ? (Spanned) text : null; 660 mTextStart = start; 661 mTextLength = end - start; 662 663 if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) { 664 mCopiedBuffer = new char[mTextLength]; 665 } 666 TextUtils.getChars(text, start, end, mCopiedBuffer, 0); 667 668 // Replace characters associated with ReplacementSpan to U+FFFC. 669 if (mSpanned != null) { 670 ReplacementSpan[] spans = mSpanned.getSpans(start, end, ReplacementSpan.class); 671 672 for (int i = 0; i < spans.length; i++) { 673 int startInPara = mSpanned.getSpanStart(spans[i]) - start; 674 int endInPara = mSpanned.getSpanEnd(spans[i]) - start; 675 // The span interval may be larger and must be restricted to [start, end) 676 if (startInPara < 0) startInPara = 0; 677 if (endInPara > mTextLength) endInPara = mTextLength; 678 Arrays.fill(mCopiedBuffer, startInPara, endInPara, OBJECT_REPLACEMENT_CHARACTER); 679 } 680 } 681 682 if (ClientFlags.icuBidiMigration()) { 683 if ((textDir == TextDirectionHeuristics.LTR 684 || textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR 685 || textDir == TextDirectionHeuristics.ANYRTL_LTR) 686 && TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) { 687 mLevels.clear(); 688 mLtrWithoutBidi = true; 689 return; 690 } 691 final int bidiRequest; 692 if (textDir == TextDirectionHeuristics.LTR) { 693 bidiRequest = Bidi.LTR; 694 } else if (textDir == TextDirectionHeuristics.RTL) { 695 bidiRequest = Bidi.RTL; 696 } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) { 697 bidiRequest = Bidi.LEVEL_DEFAULT_LTR; 698 } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) { 699 bidiRequest = Bidi.LEVEL_DEFAULT_RTL; 700 } else { 701 final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength); 702 bidiRequest = isRtl ? Bidi.RTL : Bidi.LTR; 703 } 704 mBidi = new Bidi(mCopiedBuffer, 0, null, 0, mCopiedBuffer.length, bidiRequest); 705 706 if (mCopiedBuffer.length > 0 707 && mBidi.getParagraphIndex(mCopiedBuffer.length - 1) != 0) { 708 // Historically, the MeasuredParagraph does not treat the CR letters as paragraph 709 // breaker but ICU BiDi treats it as paragraph breaker. In the MeasureParagraph, 710 // the given range always represents a single paragraph, so if the BiDi object has 711 // multiple paragraph, it should contains a CR letters in the text. Using CR is not 712 // common in Android and also it should not penalize the easy case, e.g. all LTR, 713 // check the paragraph count here and replace the CR letters and re-calculate 714 // BiDi again. 715 for (int i = 0; i < mTextLength; ++i) { 716 if (Character.isSurrogate(mCopiedBuffer[i])) { 717 // All block separators are in BMP. 718 continue; 719 } 720 if (UCharacter.getDirection(mCopiedBuffer[i]) 721 == UCharacterDirection.BLOCK_SEPARATOR) { 722 mCopiedBuffer[i] = OBJECT_REPLACEMENT_CHARACTER; 723 } 724 } 725 mBidi = new Bidi(mCopiedBuffer, 0, null, 0, mCopiedBuffer.length, bidiRequest); 726 } 727 mLevels.resize(mTextLength); 728 byte[] rawArray = mLevels.getRawArray(); 729 for (int i = 0; i < mTextLength; ++i) { 730 rawArray[i] = mBidi.getLevelAt(i); 731 } 732 mLtrWithoutBidi = false; 733 return; 734 } 735 if ((textDir == TextDirectionHeuristics.LTR 736 || textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR 737 || textDir == TextDirectionHeuristics.ANYRTL_LTR) 738 && TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) { 739 mLevels.clear(); 740 mParaDir = Layout.DIR_LEFT_TO_RIGHT; 741 mLtrWithoutBidi = true; 742 } else { 743 final int bidiRequest; 744 if (textDir == TextDirectionHeuristics.LTR) { 745 bidiRequest = Layout.DIR_REQUEST_LTR; 746 } else if (textDir == TextDirectionHeuristics.RTL) { 747 bidiRequest = Layout.DIR_REQUEST_RTL; 748 } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) { 749 bidiRequest = Layout.DIR_REQUEST_DEFAULT_LTR; 750 } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) { 751 bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL; 752 } else { 753 final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength); 754 bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR; 755 } 756 mLevels.resize(mTextLength); 757 mParaDir = AndroidBidi.bidi(bidiRequest, mCopiedBuffer, mLevels.getRawArray()); 758 mLtrWithoutBidi = false; 759 } 760 } 761 applyReplacementRun(@onNull ReplacementSpan replacement, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextPaint paint, @Nullable MeasuredText.Builder builder, @Nullable StyleRunCallback testCallback)762 private void applyReplacementRun(@NonNull ReplacementSpan replacement, 763 @IntRange(from = 0) int start, // inclusive, in copied buffer 764 @IntRange(from = 0) int end, // exclusive, in copied buffer 765 @NonNull TextPaint paint, 766 @Nullable MeasuredText.Builder builder, 767 @Nullable StyleRunCallback testCallback) { 768 // Use original text. Shouldn't matter. 769 // TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for 770 // backward compatibility? or Should we initialize them for getFontMetricsInt? 771 final float width = replacement.getSize( 772 paint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm); 773 if (builder == null) { 774 // Assigns all width to the first character. This is the same behavior as minikin. 775 mWidths.set(start, width); 776 if (end > start + 1) { 777 Arrays.fill(mWidths.getRawArray(), start + 1, end, 0.0f); 778 } 779 mWholeWidth += width; 780 } else { 781 builder.appendReplacementRun(paint, end - start, width); 782 } 783 if (testCallback != null) { 784 testCallback.onAppendReplacementRun(paint, end - start, width); 785 } 786 } 787 applyStyleRun(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull TextPaint paint, @Nullable LineBreakConfig config, @Nullable MeasuredText.Builder builder, @Nullable StyleRunCallback testCallback)788 private void applyStyleRun(@IntRange(from = 0) int start, // inclusive, in copied buffer 789 @IntRange(from = 0) int end, // exclusive, in copied buffer 790 @NonNull TextPaint paint, 791 @Nullable LineBreakConfig config, 792 @Nullable MeasuredText.Builder builder, 793 @Nullable StyleRunCallback testCallback) { 794 795 if (mLtrWithoutBidi) { 796 // If the whole text is LTR direction, just apply whole region. 797 if (builder == null) { 798 // For the compatibility reasons, the letter spacing should not be dropped at the 799 // left and right edge. 800 int oldFlag = paint.getFlags(); 801 paint.setFlags(paint.getFlags() 802 | (Paint.TEXT_RUN_FLAG_LEFT_EDGE | Paint.TEXT_RUN_FLAG_RIGHT_EDGE)); 803 try { 804 mWholeWidth += paint.getTextRunAdvances( 805 mCopiedBuffer, start, end - start, start, end - start, 806 false /* isRtl */, mWidths.getRawArray(), start); 807 } finally { 808 paint.setFlags(oldFlag); 809 } 810 } else { 811 builder.appendStyleRun(paint, config, end - start, false /* isRtl */); 812 } 813 if (testCallback != null) { 814 testCallback.onAppendStyleRun(paint, config, end - start, false); 815 } 816 } else { 817 // If there is multiple bidi levels, split into individual bidi level and apply style. 818 byte level = mLevels.get(start); 819 // Note that the empty text or empty range won't reach this method. 820 // Safe to search from start + 1. 821 for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) { 822 if (levelEnd == end || mLevels.get(levelEnd) != level) { // transition point 823 final boolean isRtl = (level & 0x1) != 0; 824 if (builder == null) { 825 final int levelLength = levelEnd - levelStart; 826 int oldFlag = paint.getFlags(); 827 paint.setFlags(paint.getFlags() 828 | (Paint.TEXT_RUN_FLAG_LEFT_EDGE | Paint.TEXT_RUN_FLAG_RIGHT_EDGE)); 829 try { 830 mWholeWidth += paint.getTextRunAdvances( 831 mCopiedBuffer, levelStart, levelLength, levelStart, levelLength, 832 isRtl, mWidths.getRawArray(), levelStart); 833 } finally { 834 paint.setFlags(oldFlag); 835 } 836 } else { 837 builder.appendStyleRun(paint, config, levelEnd - levelStart, isRtl); 838 } 839 if (testCallback != null) { 840 testCallback.onAppendStyleRun(paint, config, levelEnd - levelStart, isRtl); 841 } 842 if (levelEnd == end) { 843 break; 844 } 845 levelStart = levelEnd; 846 level = mLevels.get(levelEnd); 847 } 848 } 849 } 850 } 851 applyMetricsAffectingSpan( @onNull TextPaint paint, @Nullable LineBreakConfig lineBreakConfig, @Nullable MetricAffectingSpan[] spans, @Nullable LineBreakConfigSpan[] lbcSpans, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @Nullable MeasuredText.Builder builder, @Nullable StyleRunCallback testCallback)852 private void applyMetricsAffectingSpan( 853 @NonNull TextPaint paint, 854 @Nullable LineBreakConfig lineBreakConfig, 855 @Nullable MetricAffectingSpan[] spans, 856 @Nullable LineBreakConfigSpan[] lbcSpans, 857 @IntRange(from = 0) int start, // inclusive, in original text buffer 858 @IntRange(from = 0) int end, // exclusive, in original text buffer 859 @Nullable MeasuredText.Builder builder, 860 @Nullable StyleRunCallback testCallback) { 861 mCachedPaint.set(paint); 862 // XXX paint should not have a baseline shift, but... 863 mCachedPaint.baselineShift = 0; 864 865 final boolean needFontMetrics = builder != null; 866 867 if (needFontMetrics && mCachedFm == null) { 868 mCachedFm = new Paint.FontMetricsInt(); 869 } 870 871 ReplacementSpan replacement = null; 872 if (spans != null) { 873 for (int i = 0; i < spans.length; i++) { 874 MetricAffectingSpan span = spans[i]; 875 if (span instanceof ReplacementSpan) { 876 // The last ReplacementSpan is effective for backward compatibility reasons. 877 replacement = (ReplacementSpan) span; 878 } else { 879 // TODO: No need to call updateMeasureState for ReplacementSpan as well? 880 span.updateMeasureState(mCachedPaint); 881 } 882 } 883 } 884 885 if (lbcSpans != null) { 886 mLineBreakConfigBuilder.reset(lineBreakConfig); 887 for (LineBreakConfigSpan lbcSpan : lbcSpans) { 888 mLineBreakConfigBuilder.merge(lbcSpan.getLineBreakConfig()); 889 } 890 lineBreakConfig = mLineBreakConfigBuilder.build(); 891 } 892 893 final int startInCopiedBuffer = start - mTextStart; 894 final int endInCopiedBuffer = end - mTextStart; 895 896 if (builder != null) { 897 mCachedPaint.getFontMetricsInt(mCachedFm); 898 } 899 900 if (replacement != null) { 901 applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer, mCachedPaint, 902 builder, testCallback); 903 } else { 904 applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, mCachedPaint, 905 lineBreakConfig, builder, testCallback); 906 } 907 908 if (needFontMetrics) { 909 if (mCachedPaint.baselineShift < 0) { 910 mCachedFm.ascent += mCachedPaint.baselineShift; 911 mCachedFm.top += mCachedPaint.baselineShift; 912 } else { 913 mCachedFm.descent += mCachedPaint.baselineShift; 914 mCachedFm.bottom += mCachedPaint.baselineShift; 915 } 916 917 mFontMetrics.append(mCachedFm.top); 918 mFontMetrics.append(mCachedFm.bottom); 919 mFontMetrics.append(mCachedFm.ascent); 920 mFontMetrics.append(mCachedFm.descent); 921 } 922 } 923 924 /** 925 * Returns the maximum index that the accumulated width not exceeds the width. 926 * 927 * If forward=false is passed, returns the minimum index from the end instead. 928 * 929 * This only works if the MeasuredParagraph is computed with buildForMeasurement. 930 * Undefined behavior in other case. 931 */ breakText(int limit, boolean forwards, float width)932 @IntRange(from = 0) int breakText(int limit, boolean forwards, float width) { 933 float[] w = mWidths.getRawArray(); 934 if (forwards) { 935 int i = 0; 936 while (i < limit) { 937 width -= w[i]; 938 if (width < 0.0f) break; 939 i++; 940 } 941 while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--; 942 return i; 943 } else { 944 int i = limit - 1; 945 while (i >= 0) { 946 width -= w[i]; 947 if (width < 0.0f) break; 948 i--; 949 } 950 while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) { 951 i++; 952 } 953 return limit - i - 1; 954 } 955 } 956 957 /** 958 * Returns the length of the substring. 959 * 960 * This only works if the MeasuredParagraph is computed with buildForMeasurement. 961 * Undefined behavior in other case. 962 */ measure(int start, int limit)963 @FloatRange(from = 0.0f) float measure(int start, int limit) { 964 float width = 0; 965 float[] w = mWidths.getRawArray(); 966 for (int i = start; i < limit; ++i) { 967 width += w[i]; 968 } 969 return width; 970 } 971 972 /** 973 * This only works if the MeasuredParagraph is computed with buildForStaticLayout. 974 * @hide 975 */ getMemoryUsage()976 public @IntRange(from = 0) int getMemoryUsage() { 977 return mMeasuredText.getMemoryUsage(); 978 } 979 } 980