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