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.IntRange; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.UnsupportedAppUsage; 23 import android.graphics.Canvas; 24 import android.graphics.Paint; 25 import android.graphics.Paint.FontMetricsInt; 26 import android.os.Build; 27 import android.text.Layout.Directions; 28 import android.text.Layout.TabStops; 29 import android.text.style.CharacterStyle; 30 import android.text.style.MetricAffectingSpan; 31 import android.text.style.ReplacementSpan; 32 import android.util.Log; 33 34 import com.android.internal.annotations.VisibleForTesting; 35 import com.android.internal.util.ArrayUtils; 36 37 import java.util.ArrayList; 38 39 /** 40 * Represents a line of styled text, for measuring in visual order and 41 * for rendering. 42 * 43 * <p>Get a new instance using obtain(), and when finished with it, return it 44 * to the pool using recycle(). 45 * 46 * <p>Call set to prepare the instance for use, then either draw, measure, 47 * metrics, or caretToLeftRightOf. 48 * 49 * @hide 50 */ 51 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 52 public class TextLine { 53 private static final boolean DEBUG = false; 54 55 private static final char TAB_CHAR = '\t'; 56 57 private TextPaint mPaint; 58 @UnsupportedAppUsage 59 private CharSequence mText; 60 private int mStart; 61 private int mLen; 62 private int mDir; 63 private Directions mDirections; 64 private boolean mHasTabs; 65 private TabStops mTabs; 66 private char[] mChars; 67 private boolean mCharsValid; 68 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 69 private Spanned mSpanned; 70 private PrecomputedText mComputed; 71 72 // The start and end of a potentially existing ellipsis on this text line. 73 // We use them to filter out replacement and metric affecting spans on ellipsized away chars. 74 private int mEllipsisStart; 75 private int mEllipsisEnd; 76 77 // Additional width of whitespace for justification. This value is per whitespace, thus 78 // the line width will increase by mAddedWidthForJustify x (number of stretchable whitespaces). 79 private float mAddedWidthForJustify; 80 private boolean mIsJustifying; 81 82 private final TextPaint mWorkPaint = new TextPaint(); 83 private final TextPaint mActivePaint = new TextPaint(); 84 @UnsupportedAppUsage 85 private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet = 86 new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class); 87 @UnsupportedAppUsage 88 private final SpanSet<CharacterStyle> mCharacterStyleSpanSet = 89 new SpanSet<CharacterStyle>(CharacterStyle.class); 90 @UnsupportedAppUsage 91 private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet = 92 new SpanSet<ReplacementSpan>(ReplacementSpan.class); 93 94 private final DecorationInfo mDecorationInfo = new DecorationInfo(); 95 private final ArrayList<DecorationInfo> mDecorations = new ArrayList<>(); 96 97 /** Not allowed to access. If it's for memory leak workaround, it was already fixed M. */ 98 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) 99 private static final TextLine[] sCached = new TextLine[3]; 100 101 /** 102 * Returns a new TextLine from the shared pool. 103 * 104 * @return an uninitialized TextLine 105 */ 106 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 107 @UnsupportedAppUsage obtain()108 public static TextLine obtain() { 109 TextLine tl; 110 synchronized (sCached) { 111 for (int i = sCached.length; --i >= 0;) { 112 if (sCached[i] != null) { 113 tl = sCached[i]; 114 sCached[i] = null; 115 return tl; 116 } 117 } 118 } 119 tl = new TextLine(); 120 if (DEBUG) { 121 Log.v("TLINE", "new: " + tl); 122 } 123 return tl; 124 } 125 126 /** 127 * Puts a TextLine back into the shared pool. Do not use this TextLine once 128 * it has been returned. 129 * @param tl the textLine 130 * @return null, as a convenience from clearing references to the provided 131 * TextLine 132 */ 133 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) recycle(TextLine tl)134 public static TextLine recycle(TextLine tl) { 135 tl.mText = null; 136 tl.mPaint = null; 137 tl.mDirections = null; 138 tl.mSpanned = null; 139 tl.mTabs = null; 140 tl.mChars = null; 141 tl.mComputed = null; 142 143 tl.mMetricAffectingSpanSpanSet.recycle(); 144 tl.mCharacterStyleSpanSet.recycle(); 145 tl.mReplacementSpanSpanSet.recycle(); 146 147 synchronized(sCached) { 148 for (int i = 0; i < sCached.length; ++i) { 149 if (sCached[i] == null) { 150 sCached[i] = tl; 151 break; 152 } 153 } 154 } 155 return null; 156 } 157 158 /** 159 * Initializes a TextLine and prepares it for use. 160 * 161 * @param paint the base paint for the line 162 * @param text the text, can be Styled 163 * @param start the start of the line relative to the text 164 * @param limit the limit of the line relative to the text 165 * @param dir the paragraph direction of this line 166 * @param directions the directions information of this line 167 * @param hasTabs true if the line might contain tabs 168 * @param tabStops the tabStops. Can be null 169 * @param ellipsisStart the start of the ellipsis relative to the line 170 * @param ellipsisEnd the end of the ellipsis relative to the line. When there 171 * is no ellipsis, this should be equal to ellipsisStart. 172 */ 173 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) set(TextPaint paint, CharSequence text, int start, int limit, int dir, Directions directions, boolean hasTabs, TabStops tabStops, int ellipsisStart, int ellipsisEnd)174 public void set(TextPaint paint, CharSequence text, int start, int limit, int dir, 175 Directions directions, boolean hasTabs, TabStops tabStops, 176 int ellipsisStart, int ellipsisEnd) { 177 mPaint = paint; 178 mText = text; 179 mStart = start; 180 mLen = limit - start; 181 mDir = dir; 182 mDirections = directions; 183 if (mDirections == null) { 184 throw new IllegalArgumentException("Directions cannot be null"); 185 } 186 mHasTabs = hasTabs; 187 mSpanned = null; 188 189 boolean hasReplacement = false; 190 if (text instanceof Spanned) { 191 mSpanned = (Spanned) text; 192 mReplacementSpanSpanSet.init(mSpanned, start, limit); 193 hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0; 194 } 195 196 mComputed = null; 197 if (text instanceof PrecomputedText) { 198 // Here, no need to check line break strategy or hyphenation frequency since there is no 199 // line break concept here. 200 mComputed = (PrecomputedText) text; 201 if (!mComputed.getParams().getTextPaint().equalsForTextMeasurement(paint)) { 202 mComputed = null; 203 } 204 } 205 206 mCharsValid = hasReplacement; 207 208 if (mCharsValid) { 209 if (mChars == null || mChars.length < mLen) { 210 mChars = ArrayUtils.newUnpaddedCharArray(mLen); 211 } 212 TextUtils.getChars(text, start, limit, mChars, 0); 213 if (hasReplacement) { 214 // Handle these all at once so we don't have to do it as we go. 215 // Replace the first character of each replacement run with the 216 // object-replacement character and the remainder with zero width 217 // non-break space aka BOM. Cursor movement code skips these 218 // zero-width characters. 219 char[] chars = mChars; 220 for (int i = start, inext; i < limit; i = inext) { 221 inext = mReplacementSpanSpanSet.getNextTransition(i, limit); 222 if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext) 223 && (i - start >= ellipsisEnd || inext - start <= ellipsisStart)) { 224 // transition into a span 225 chars[i - start] = '\ufffc'; 226 for (int j = i - start + 1, e = inext - start; j < e; ++j) { 227 chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip 228 } 229 } 230 } 231 } 232 } 233 mTabs = tabStops; 234 mAddedWidthForJustify = 0; 235 mIsJustifying = false; 236 237 mEllipsisStart = ellipsisStart != ellipsisEnd ? ellipsisStart : 0; 238 mEllipsisEnd = ellipsisStart != ellipsisEnd ? ellipsisEnd : 0; 239 } 240 charAt(int i)241 private char charAt(int i) { 242 return mCharsValid ? mChars[i] : mText.charAt(i + mStart); 243 } 244 245 /** 246 * Justify the line to the given width. 247 */ 248 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) justify(float justifyWidth)249 public void justify(float justifyWidth) { 250 int end = mLen; 251 while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) { 252 end--; 253 } 254 final int spaces = countStretchableSpaces(0, end); 255 if (spaces == 0) { 256 // There are no stretchable spaces, so we can't help the justification by adding any 257 // width. 258 return; 259 } 260 final float width = Math.abs(measure(end, false, null)); 261 mAddedWidthForJustify = (justifyWidth - width) / spaces; 262 mIsJustifying = true; 263 } 264 265 /** 266 * Renders the TextLine. 267 * 268 * @param c the canvas to render on 269 * @param x the leading margin position 270 * @param top the top of the line 271 * @param y the baseline 272 * @param bottom the bottom of the line 273 */ draw(Canvas c, float x, int top, int y, int bottom)274 void draw(Canvas c, float x, int top, int y, int bottom) { 275 float h = 0; 276 final int runCount = mDirections.getRunCount(); 277 for (int runIndex = 0; runIndex < runCount; runIndex++) { 278 final int runStart = mDirections.getRunStart(runIndex); 279 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); 280 final boolean runIsRtl = mDirections.isRunRtl(runIndex); 281 282 int segStart = runStart; 283 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { 284 if (j == runLimit || charAt(j) == TAB_CHAR) { 285 h += drawRun(c, segStart, j, runIsRtl, x + h, top, y, bottom, 286 runIndex != (runCount - 1) || j != mLen); 287 288 if (j != runLimit) { // charAt(j) == TAB_CHAR 289 h = mDir * nextTab(h * mDir); 290 } 291 segStart = j + 1; 292 } 293 } 294 } 295 } 296 297 /** 298 * Returns metrics information for the entire line. 299 * 300 * @param fmi receives font metrics information, can be null 301 * @return the signed width of the line 302 */ 303 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) metrics(FontMetricsInt fmi)304 public float metrics(FontMetricsInt fmi) { 305 return measure(mLen, false, fmi); 306 } 307 308 /** 309 * Returns the signed graphical offset from the leading margin. 310 * 311 * Following examples are all for measuring offset=3. LX(e.g. L0, L1, ...) denotes a 312 * character which has LTR BiDi property. On the other hand, RX(e.g. R0, R1, ...) denotes a 313 * character which has RTL BiDi property. Assuming all character has 1em width. 314 * 315 * Example 1: All LTR chars within LTR context 316 * Input Text (logical) : L0 L1 L2 L3 L4 L5 L6 L7 L8 317 * Input Text (visual) : L0 L1 L2 L3 L4 L5 L6 L7 L8 318 * Output(trailing=true) : |--------| (Returns 3em) 319 * Output(trailing=false): |--------| (Returns 3em) 320 * 321 * Example 2: All RTL chars within RTL context. 322 * Input Text (logical) : R0 R1 R2 R3 R4 R5 R6 R7 R8 323 * Input Text (visual) : R8 R7 R6 R5 R4 R3 R2 R1 R0 324 * Output(trailing=true) : |--------| (Returns -3em) 325 * Output(trailing=false): |--------| (Returns -3em) 326 * 327 * Example 3: BiDi chars within LTR context. 328 * Input Text (logical) : L0 L1 L2 R3 R4 R5 L6 L7 L8 329 * Input Text (visual) : L0 L1 L2 R5 R4 R3 L6 L7 L8 330 * Output(trailing=true) : |-----------------| (Returns 6em) 331 * Output(trailing=false): |--------| (Returns 3em) 332 * 333 * Example 4: BiDi chars within RTL context. 334 * Input Text (logical) : L0 L1 L2 R3 R4 R5 L6 L7 L8 335 * Input Text (visual) : L6 L7 L8 R5 R4 R3 L0 L1 L2 336 * Output(trailing=true) : |-----------------| (Returns -6em) 337 * Output(trailing=false): |--------| (Returns -3em) 338 * 339 * @param offset the line-relative character offset, between 0 and the line length, inclusive 340 * @param trailing no effect if the offset is not on the BiDi transition offset. If the offset 341 * is on the BiDi transition offset and true is passed, the offset is regarded 342 * as the edge of the trailing run's edge. If false, the offset is regarded as 343 * the edge of the preceding run's edge. See example above. 344 * @param fmi receives metrics information about the requested character, can be null 345 * @return the signed graphical offset from the leading margin to the requested character edge. 346 * The positive value means the offset is right from the leading edge. The negative 347 * value means the offset is left from the leading edge. 348 */ measure(@ntRangefrom = 0) int offset, boolean trailing, @NonNull FontMetricsInt fmi)349 public float measure(@IntRange(from = 0) int offset, boolean trailing, 350 @NonNull FontMetricsInt fmi) { 351 if (offset > mLen) { 352 throw new IndexOutOfBoundsException( 353 "offset(" + offset + ") should be less than line limit(" + mLen + ")"); 354 } 355 final int target = trailing ? offset - 1 : offset; 356 if (target < 0) { 357 return 0; 358 } 359 360 float h = 0; 361 for (int runIndex = 0; runIndex < mDirections.getRunCount(); runIndex++) { 362 final int runStart = mDirections.getRunStart(runIndex); 363 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); 364 final boolean runIsRtl = mDirections.isRunRtl(runIndex); 365 366 int segStart = runStart; 367 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { 368 if (j == runLimit || charAt(j) == TAB_CHAR) { 369 final boolean targetIsInThisSegment = target >= segStart && target < j; 370 final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl; 371 372 if (targetIsInThisSegment && sameDirection) { 373 return h + measureRun(segStart, offset, j, runIsRtl, fmi); 374 } 375 376 final float segmentWidth = measureRun(segStart, j, j, runIsRtl, fmi); 377 h += sameDirection ? segmentWidth : -segmentWidth; 378 379 if (targetIsInThisSegment) { 380 return h + measureRun(segStart, offset, j, runIsRtl, null); 381 } 382 383 if (j != runLimit) { // charAt(j) == TAB_CHAR 384 if (offset == j) { 385 return h; 386 } 387 h = mDir * nextTab(h * mDir); 388 if (target == j) { 389 return h; 390 } 391 } 392 393 segStart = j + 1; 394 } 395 } 396 } 397 398 return h; 399 } 400 401 /** 402 * @see #measure(int, boolean, FontMetricsInt) 403 * @return The measure results for all possible offsets 404 */ 405 @VisibleForTesting 406 public float[] measureAllOffsets(boolean[] trailing, FontMetricsInt fmi) { 407 float[] measurement = new float[mLen + 1]; 408 409 int[] target = new int[mLen + 1]; 410 for (int offset = 0; offset < target.length; ++offset) { 411 target[offset] = trailing[offset] ? offset - 1 : offset; 412 } 413 if (target[0] < 0) { 414 measurement[0] = 0; 415 } 416 417 float h = 0; 418 for (int runIndex = 0; runIndex < mDirections.getRunCount(); runIndex++) { 419 final int runStart = mDirections.getRunStart(runIndex); 420 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); 421 final boolean runIsRtl = mDirections.isRunRtl(runIndex); 422 423 int segStart = runStart; 424 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; ++j) { 425 if (j == runLimit || charAt(j) == TAB_CHAR) { 426 final float oldh = h; 427 final boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl; 428 final float w = measureRun(segStart, j, j, runIsRtl, fmi); 429 h += advance ? w : -w; 430 431 final float baseh = advance ? oldh : h; 432 FontMetricsInt crtfmi = advance ? fmi : null; 433 for (int offset = segStart; offset <= j && offset <= mLen; ++offset) { 434 if (target[offset] >= segStart && target[offset] < j) { 435 measurement[offset] = 436 baseh + measureRun(segStart, offset, j, runIsRtl, crtfmi); 437 } 438 } 439 440 if (j != runLimit) { // charAt(j) == TAB_CHAR 441 if (target[j] == j) { 442 measurement[j] = h; 443 } 444 h = mDir * nextTab(h * mDir); 445 if (target[j + 1] == j) { 446 measurement[j + 1] = h; 447 } 448 } 449 450 segStart = j + 1; 451 } 452 } 453 } 454 if (target[mLen] == mLen) { 455 measurement[mLen] = h; 456 } 457 458 return measurement; 459 } 460 461 /** 462 * Draws a unidirectional (but possibly multi-styled) run of text. 463 * 464 * 465 * @param c the canvas to draw on 466 * @param start the line-relative start 467 * @param limit the line-relative limit 468 * @param runIsRtl true if the run is right-to-left 469 * @param x the position of the run that is closest to the leading margin 470 * @param top the top of the line 471 * @param y the baseline 472 * @param bottom the bottom of the line 473 * @param needWidth true if the width value is required. 474 * @return the signed width of the run, based on the paragraph direction. 475 * Only valid if needWidth is true. 476 */ 477 private float drawRun(Canvas c, int start, 478 int limit, boolean runIsRtl, float x, int top, int y, int bottom, 479 boolean needWidth) { 480 481 if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { 482 float w = -measureRun(start, limit, limit, runIsRtl, null); 483 handleRun(start, limit, limit, runIsRtl, c, x + w, top, 484 y, bottom, null, false); 485 return w; 486 } 487 488 return handleRun(start, limit, limit, runIsRtl, c, x, top, 489 y, bottom, null, needWidth); 490 } 491 492 /** 493 * Measures a unidirectional (but possibly multi-styled) run of text. 494 * 495 * 496 * @param start the line-relative start of the run 497 * @param offset the offset to measure to, between start and limit inclusive 498 * @param limit the line-relative limit of the run 499 * @param runIsRtl true if the run is right-to-left 500 * @param fmi receives metrics information about the requested 501 * run, can be null. 502 * @return the signed width from the start of the run to the leading edge 503 * of the character at offset, based on the run (not paragraph) direction 504 */ 505 private float measureRun(int start, int offset, int limit, boolean runIsRtl, 506 FontMetricsInt fmi) { 507 return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true); 508 } 509 510 /** 511 * Walk the cursor through this line, skipping conjuncts and 512 * zero-width characters. 513 * 514 * <p>This function cannot properly walk the cursor off the ends of the line 515 * since it does not know about any shaping on the previous/following line 516 * that might affect the cursor position. Callers must either avoid these 517 * situations or handle the result specially. 518 * 519 * @param cursor the starting position of the cursor, between 0 and the 520 * length of the line, inclusive 521 * @param toLeft true if the caret is moving to the left. 522 * @return the new offset. If it is less than 0 or greater than the length 523 * of the line, the previous/following line should be examined to get the 524 * actual offset. 525 */ 526 int getOffsetToLeftRightOf(int cursor, boolean toLeft) { 527 // 1) The caret marks the leading edge of a character. The character 528 // logically before it might be on a different level, and the active caret 529 // position is on the character at the lower level. If that character 530 // was the previous character, the caret is on its trailing edge. 531 // 2) Take this character/edge and move it in the indicated direction. 532 // This gives you a new character and a new edge. 533 // 3) This position is between two visually adjacent characters. One of 534 // these might be at a lower level. The active position is on the 535 // character at the lower level. 536 // 4) If the active position is on the trailing edge of the character, 537 // the new caret position is the following logical character, else it 538 // is the character. 539 540 int lineStart = 0; 541 int lineEnd = mLen; 542 boolean paraIsRtl = mDir == -1; 543 int[] runs = mDirections.mDirections; 544 545 int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1; 546 boolean trailing = false; 547 548 if (cursor == lineStart) { 549 runIndex = -2; 550 } else if (cursor == lineEnd) { 551 runIndex = runs.length; 552 } else { 553 // First, get information about the run containing the character with 554 // the active caret. 555 for (runIndex = 0; runIndex < runs.length; runIndex += 2) { 556 runStart = lineStart + runs[runIndex]; 557 if (cursor >= runStart) { 558 runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK); 559 if (runLimit > lineEnd) { 560 runLimit = lineEnd; 561 } 562 if (cursor < runLimit) { 563 runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) & 564 Layout.RUN_LEVEL_MASK; 565 if (cursor == runStart) { 566 // The caret is on a run boundary, see if we should 567 // use the position on the trailing edge of the previous 568 // logical character instead. 569 int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit; 570 int pos = cursor - 1; 571 for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) { 572 prevRunStart = lineStart + runs[prevRunIndex]; 573 if (pos >= prevRunStart) { 574 prevRunLimit = prevRunStart + 575 (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK); 576 if (prevRunLimit > lineEnd) { 577 prevRunLimit = lineEnd; 578 } 579 if (pos < prevRunLimit) { 580 prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) 581 & Layout.RUN_LEVEL_MASK; 582 if (prevRunLevel < runLevel) { 583 // Start from logically previous character. 584 runIndex = prevRunIndex; 585 runLevel = prevRunLevel; 586 runStart = prevRunStart; 587 runLimit = prevRunLimit; 588 trailing = true; 589 break; 590 } 591 } 592 } 593 } 594 } 595 break; 596 } 597 } 598 } 599 600 // caret might be == lineEnd. This is generally a space or paragraph 601 // separator and has an associated run, but might be the end of 602 // text, in which case it doesn't. If that happens, we ran off the 603 // end of the run list, and runIndex == runs.length. In this case, 604 // we are at a run boundary so we skip the below test. 605 if (runIndex != runs.length) { 606 boolean runIsRtl = (runLevel & 0x1) != 0; 607 boolean advance = toLeft == runIsRtl; 608 if (cursor != (advance ? runLimit : runStart) || advance != trailing) { 609 // Moving within or into the run, so we can move logically. 610 newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit, 611 runIsRtl, cursor, advance); 612 // If the new position is internal to the run, we're at the strong 613 // position already so we're finished. 614 if (newCaret != (advance ? runLimit : runStart)) { 615 return newCaret; 616 } 617 } 618 } 619 } 620 621 // If newCaret is -1, we're starting at a run boundary and crossing 622 // into another run. Otherwise we've arrived at a run boundary, and 623 // need to figure out which character to attach to. Note we might 624 // need to run this twice, if we cross a run boundary and end up at 625 // another run boundary. 626 while (true) { 627 boolean advance = toLeft == paraIsRtl; 628 int otherRunIndex = runIndex + (advance ? 2 : -2); 629 if (otherRunIndex >= 0 && otherRunIndex < runs.length) { 630 int otherRunStart = lineStart + runs[otherRunIndex]; 631 int otherRunLimit = otherRunStart + 632 (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK); 633 if (otherRunLimit > lineEnd) { 634 otherRunLimit = lineEnd; 635 } 636 int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) & 637 Layout.RUN_LEVEL_MASK; 638 boolean otherRunIsRtl = (otherRunLevel & 1) != 0; 639 640 advance = toLeft == otherRunIsRtl; 641 if (newCaret == -1) { 642 newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart, 643 otherRunLimit, otherRunIsRtl, 644 advance ? otherRunStart : otherRunLimit, advance); 645 if (newCaret == (advance ? otherRunLimit : otherRunStart)) { 646 // Crossed and ended up at a new boundary, 647 // repeat a second and final time. 648 runIndex = otherRunIndex; 649 runLevel = otherRunLevel; 650 continue; 651 } 652 break; 653 } 654 655 // The new caret is at a boundary. 656 if (otherRunLevel < runLevel) { 657 // The strong character is in the other run. 658 newCaret = advance ? otherRunStart : otherRunLimit; 659 } 660 break; 661 } 662 663 if (newCaret == -1) { 664 // We're walking off the end of the line. The paragraph 665 // level is always equal to or lower than any internal level, so 666 // the boundaries get the strong caret. 667 newCaret = advance ? mLen + 1 : -1; 668 break; 669 } 670 671 // Else we've arrived at the end of the line. That's a strong position. 672 // We might have arrived here by crossing over a run with no internal 673 // breaks and dropping out of the above loop before advancing one final 674 // time, so reset the caret. 675 // Note, we use '<=' below to handle a situation where the only run 676 // on the line is a counter-directional run. If we're not advancing, 677 // we can end up at the 'lineEnd' position but the caret we want is at 678 // the lineStart. 679 if (newCaret <= lineEnd) { 680 newCaret = advance ? lineEnd : lineStart; 681 } 682 break; 683 } 684 685 return newCaret; 686 } 687 688 /** 689 * Returns the next valid offset within this directional run, skipping 690 * conjuncts and zero-width characters. This should not be called to walk 691 * off the end of the line, since the returned values might not be valid 692 * on neighboring lines. If the returned offset is less than zero or 693 * greater than the line length, the offset should be recomputed on the 694 * preceding or following line, respectively. 695 * 696 * @param runIndex the run index 697 * @param runStart the start of the run 698 * @param runLimit the limit of the run 699 * @param runIsRtl true if the run is right-to-left 700 * @param offset the offset 701 * @param after true if the new offset should logically follow the provided 702 * offset 703 * @return the new offset 704 */ getOffsetBeforeAfter(int runIndex, int runStart, int runLimit, boolean runIsRtl, int offset, boolean after)705 private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit, 706 boolean runIsRtl, int offset, boolean after) { 707 708 if (runIndex < 0 || offset == (after ? mLen : 0)) { 709 // Walking off end of line. Since we don't know 710 // what cursor positions are available on other lines, we can't 711 // return accurate values. These are a guess. 712 if (after) { 713 return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart; 714 } 715 return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart; 716 } 717 718 TextPaint wp = mWorkPaint; 719 wp.set(mPaint); 720 if (mIsJustifying) { 721 wp.setWordSpacing(mAddedWidthForJustify); 722 } 723 724 int spanStart = runStart; 725 int spanLimit; 726 if (mSpanned == null) { 727 spanLimit = runLimit; 728 } else { 729 int target = after ? offset + 1 : offset; 730 int limit = mStart + runLimit; 731 while (true) { 732 spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit, 733 MetricAffectingSpan.class) - mStart; 734 if (spanLimit >= target) { 735 break; 736 } 737 spanStart = spanLimit; 738 } 739 740 MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart, 741 mStart + spanLimit, MetricAffectingSpan.class); 742 spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class); 743 744 if (spans.length > 0) { 745 ReplacementSpan replacement = null; 746 for (int j = 0; j < spans.length; j++) { 747 MetricAffectingSpan span = spans[j]; 748 if (span instanceof ReplacementSpan) { 749 replacement = (ReplacementSpan)span; 750 } else { 751 span.updateMeasureState(wp); 752 } 753 } 754 755 if (replacement != null) { 756 // If we have a replacement span, we're moving either to 757 // the start or end of this span. 758 return after ? spanLimit : spanStart; 759 } 760 } 761 } 762 763 int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE; 764 if (mCharsValid) { 765 return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart, 766 runIsRtl, offset, cursorOpt); 767 } else { 768 return wp.getTextRunCursor(mText, mStart + spanStart, 769 mStart + spanLimit, runIsRtl, mStart + offset, cursorOpt) - mStart; 770 } 771 } 772 773 /** 774 * @param wp 775 */ expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp)776 private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) { 777 final int previousTop = fmi.top; 778 final int previousAscent = fmi.ascent; 779 final int previousDescent = fmi.descent; 780 final int previousBottom = fmi.bottom; 781 final int previousLeading = fmi.leading; 782 783 wp.getFontMetricsInt(fmi); 784 785 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom, 786 previousLeading); 787 } 788 updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent, int previousDescent, int previousBottom, int previousLeading)789 static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent, 790 int previousDescent, int previousBottom, int previousLeading) { 791 fmi.top = Math.min(fmi.top, previousTop); 792 fmi.ascent = Math.min(fmi.ascent, previousAscent); 793 fmi.descent = Math.max(fmi.descent, previousDescent); 794 fmi.bottom = Math.max(fmi.bottom, previousBottom); 795 fmi.leading = Math.max(fmi.leading, previousLeading); 796 } 797 drawStroke(TextPaint wp, Canvas c, int color, float position, float thickness, float xleft, float xright, float baseline)798 private static void drawStroke(TextPaint wp, Canvas c, int color, float position, 799 float thickness, float xleft, float xright, float baseline) { 800 final float strokeTop = baseline + wp.baselineShift + position; 801 802 final int previousColor = wp.getColor(); 803 final Paint.Style previousStyle = wp.getStyle(); 804 final boolean previousAntiAlias = wp.isAntiAlias(); 805 806 wp.setStyle(Paint.Style.FILL); 807 wp.setAntiAlias(true); 808 809 wp.setColor(color); 810 c.drawRect(xleft, strokeTop, xright, strokeTop + thickness, wp); 811 812 wp.setStyle(previousStyle); 813 wp.setColor(previousColor); 814 wp.setAntiAlias(previousAntiAlias); 815 } 816 getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, int offset)817 private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd, 818 boolean runIsRtl, int offset) { 819 if (mCharsValid) { 820 return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset); 821 } else { 822 final int delta = mStart; 823 if (mComputed == null) { 824 return wp.getRunAdvance(mText, delta + start, delta + end, 825 delta + contextStart, delta + contextEnd, runIsRtl, delta + offset); 826 } else { 827 return mComputed.getWidth(start + delta, end + delta); 828 } 829 } 830 } 831 832 /** 833 * Utility function for measuring and rendering text. The text must 834 * not include a tab. 835 * 836 * @param wp the working paint 837 * @param start the start of the text 838 * @param end the end of the text 839 * @param runIsRtl true if the run is right-to-left 840 * @param c the canvas, can be null if rendering is not needed 841 * @param x the edge of the run closest to the leading margin 842 * @param top the top of the line 843 * @param y the baseline 844 * @param bottom the bottom of the line 845 * @param fmi receives metrics information, can be null 846 * @param needWidth true if the width of the run is needed 847 * @param offset the offset for the purpose of measuring 848 * @param decorations the list of locations and paremeters for drawing decorations 849 * @return the signed width of the run based on the run direction; only 850 * valid if needWidth is true 851 */ handleText(TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, Canvas c, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth, int offset, @Nullable ArrayList<DecorationInfo> decorations)852 private float handleText(TextPaint wp, int start, int end, 853 int contextStart, int contextEnd, boolean runIsRtl, 854 Canvas c, float x, int top, int y, int bottom, 855 FontMetricsInt fmi, boolean needWidth, int offset, 856 @Nullable ArrayList<DecorationInfo> decorations) { 857 858 if (mIsJustifying) { 859 wp.setWordSpacing(mAddedWidthForJustify); 860 } 861 // Get metrics first (even for empty strings or "0" width runs) 862 if (fmi != null) { 863 expandMetricsFromPaint(fmi, wp); 864 } 865 866 // No need to do anything if the run width is "0" 867 if (end == start) { 868 return 0f; 869 } 870 871 float totalWidth = 0; 872 873 final int numDecorations = decorations == null ? 0 : decorations.size(); 874 if (needWidth || (c != null && (wp.bgColor != 0 || numDecorations != 0 || runIsRtl))) { 875 totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset); 876 } 877 878 if (c != null) { 879 final float leftX, rightX; 880 if (runIsRtl) { 881 leftX = x - totalWidth; 882 rightX = x; 883 } else { 884 leftX = x; 885 rightX = x + totalWidth; 886 } 887 888 if (wp.bgColor != 0) { 889 int previousColor = wp.getColor(); 890 Paint.Style previousStyle = wp.getStyle(); 891 892 wp.setColor(wp.bgColor); 893 wp.setStyle(Paint.Style.FILL); 894 c.drawRect(leftX, top, rightX, bottom, wp); 895 896 wp.setStyle(previousStyle); 897 wp.setColor(previousColor); 898 } 899 900 drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl, 901 leftX, y + wp.baselineShift); 902 903 if (numDecorations != 0) { 904 for (int i = 0; i < numDecorations; i++) { 905 final DecorationInfo info = decorations.get(i); 906 907 final int decorationStart = Math.max(info.start, start); 908 final int decorationEnd = Math.min(info.end, offset); 909 float decorationStartAdvance = getRunAdvance( 910 wp, start, end, contextStart, contextEnd, runIsRtl, decorationStart); 911 float decorationEndAdvance = getRunAdvance( 912 wp, start, end, contextStart, contextEnd, runIsRtl, decorationEnd); 913 final float decorationXLeft, decorationXRight; 914 if (runIsRtl) { 915 decorationXLeft = rightX - decorationEndAdvance; 916 decorationXRight = rightX - decorationStartAdvance; 917 } else { 918 decorationXLeft = leftX + decorationStartAdvance; 919 decorationXRight = leftX + decorationEndAdvance; 920 } 921 922 // Theoretically, there could be cases where both Paint's and TextPaint's 923 // setUnderLineText() are called. For backward compatibility, we need to draw 924 // both underlines, the one with custom color first. 925 if (info.underlineColor != 0) { 926 drawStroke(wp, c, info.underlineColor, wp.getUnderlinePosition(), 927 info.underlineThickness, decorationXLeft, decorationXRight, y); 928 } 929 if (info.isUnderlineText) { 930 final float thickness = 931 Math.max(wp.getUnderlineThickness(), 1.0f); 932 drawStroke(wp, c, wp.getColor(), wp.getUnderlinePosition(), thickness, 933 decorationXLeft, decorationXRight, y); 934 } 935 936 if (info.isStrikeThruText) { 937 final float thickness = 938 Math.max(wp.getStrikeThruThickness(), 1.0f); 939 drawStroke(wp, c, wp.getColor(), wp.getStrikeThruPosition(), thickness, 940 decorationXLeft, decorationXRight, y); 941 } 942 } 943 } 944 945 } 946 947 return runIsRtl ? -totalWidth : totalWidth; 948 } 949 950 /** 951 * Utility function for measuring and rendering a replacement. 952 * 953 * 954 * @param replacement the replacement 955 * @param wp the work paint 956 * @param start the start of the run 957 * @param limit the limit of the run 958 * @param runIsRtl true if the run is right-to-left 959 * @param c the canvas, can be null if not rendering 960 * @param x the edge of the replacement closest to the leading margin 961 * @param top the top of the line 962 * @param y the baseline 963 * @param bottom the bottom of the line 964 * @param fmi receives metrics information, can be null 965 * @param needWidth true if the width of the replacement is needed 966 * @return the signed width of the run based on the run direction; only 967 * valid if needWidth is true 968 */ handleReplacement(ReplacementSpan replacement, TextPaint wp, int start, int limit, boolean runIsRtl, Canvas c, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth)969 private float handleReplacement(ReplacementSpan replacement, TextPaint wp, 970 int start, int limit, boolean runIsRtl, Canvas c, 971 float x, int top, int y, int bottom, FontMetricsInt fmi, 972 boolean needWidth) { 973 974 float ret = 0; 975 976 int textStart = mStart + start; 977 int textLimit = mStart + limit; 978 979 if (needWidth || (c != null && runIsRtl)) { 980 int previousTop = 0; 981 int previousAscent = 0; 982 int previousDescent = 0; 983 int previousBottom = 0; 984 int previousLeading = 0; 985 986 boolean needUpdateMetrics = (fmi != null); 987 988 if (needUpdateMetrics) { 989 previousTop = fmi.top; 990 previousAscent = fmi.ascent; 991 previousDescent = fmi.descent; 992 previousBottom = fmi.bottom; 993 previousLeading = fmi.leading; 994 } 995 996 ret = replacement.getSize(wp, mText, textStart, textLimit, fmi); 997 998 if (needUpdateMetrics) { 999 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom, 1000 previousLeading); 1001 } 1002 } 1003 1004 if (c != null) { 1005 if (runIsRtl) { 1006 x -= ret; 1007 } 1008 replacement.draw(c, mText, textStart, textLimit, 1009 x, top, y, bottom, wp); 1010 } 1011 1012 return runIsRtl ? -ret : ret; 1013 } 1014 adjustStartHyphenEdit(int start, @Paint.StartHyphenEdit int startHyphenEdit)1015 private int adjustStartHyphenEdit(int start, @Paint.StartHyphenEdit int startHyphenEdit) { 1016 // Only draw hyphens on first in line. Disable them otherwise. 1017 return start > 0 ? Paint.START_HYPHEN_EDIT_NO_EDIT : startHyphenEdit; 1018 } 1019 adjustEndHyphenEdit(int limit, @Paint.EndHyphenEdit int endHyphenEdit)1020 private int adjustEndHyphenEdit(int limit, @Paint.EndHyphenEdit int endHyphenEdit) { 1021 // Only draw hyphens on last run in line. Disable them otherwise. 1022 return limit < mLen ? Paint.END_HYPHEN_EDIT_NO_EDIT : endHyphenEdit; 1023 } 1024 1025 private static final class DecorationInfo { 1026 public boolean isStrikeThruText; 1027 public boolean isUnderlineText; 1028 public int underlineColor; 1029 public float underlineThickness; 1030 public int start = -1; 1031 public int end = -1; 1032 hasDecoration()1033 public boolean hasDecoration() { 1034 return isStrikeThruText || isUnderlineText || underlineColor != 0; 1035 } 1036 1037 // Copies the info, but not the start and end range. copyInfo()1038 public DecorationInfo copyInfo() { 1039 final DecorationInfo copy = new DecorationInfo(); 1040 copy.isStrikeThruText = isStrikeThruText; 1041 copy.isUnderlineText = isUnderlineText; 1042 copy.underlineColor = underlineColor; 1043 copy.underlineThickness = underlineThickness; 1044 return copy; 1045 } 1046 } 1047 extractDecorationInfo(@onNull TextPaint paint, @NonNull DecorationInfo info)1048 private void extractDecorationInfo(@NonNull TextPaint paint, @NonNull DecorationInfo info) { 1049 info.isStrikeThruText = paint.isStrikeThruText(); 1050 if (info.isStrikeThruText) { 1051 paint.setStrikeThruText(false); 1052 } 1053 info.isUnderlineText = paint.isUnderlineText(); 1054 if (info.isUnderlineText) { 1055 paint.setUnderlineText(false); 1056 } 1057 info.underlineColor = paint.underlineColor; 1058 info.underlineThickness = paint.underlineThickness; 1059 paint.setUnderlineText(0, 0.0f); 1060 } 1061 1062 /** 1063 * Utility function for handling a unidirectional run. The run must not 1064 * contain tabs but can contain styles. 1065 * 1066 * 1067 * @param start the line-relative start of the run 1068 * @param measureLimit the offset to measure to, between start and limit inclusive 1069 * @param limit the limit of the run 1070 * @param runIsRtl true if the run is right-to-left 1071 * @param c the canvas, can be null 1072 * @param x the end of the run closest to the leading margin 1073 * @param top the top of the line 1074 * @param y the baseline 1075 * @param bottom the bottom of the line 1076 * @param fmi receives metrics information, can be null 1077 * @param needWidth true if the width is required 1078 * @return the signed width of the run based on the run direction; only 1079 * valid if needWidth is true 1080 */ handleRun(int start, int measureLimit, int limit, boolean runIsRtl, Canvas c, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth)1081 private float handleRun(int start, int measureLimit, 1082 int limit, boolean runIsRtl, Canvas c, float x, int top, int y, 1083 int bottom, FontMetricsInt fmi, boolean needWidth) { 1084 1085 if (measureLimit < start || measureLimit > limit) { 1086 throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of " 1087 + "start (" + start + ") and limit (" + limit + ") bounds"); 1088 } 1089 1090 // Case of an empty line, make sure we update fmi according to mPaint 1091 if (start == measureLimit) { 1092 final TextPaint wp = mWorkPaint; 1093 wp.set(mPaint); 1094 if (fmi != null) { 1095 expandMetricsFromPaint(fmi, wp); 1096 } 1097 return 0f; 1098 } 1099 1100 final boolean needsSpanMeasurement; 1101 if (mSpanned == null) { 1102 needsSpanMeasurement = false; 1103 } else { 1104 mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit); 1105 mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit); 1106 needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0 1107 || mCharacterStyleSpanSet.numberOfSpans != 0; 1108 } 1109 1110 if (!needsSpanMeasurement) { 1111 final TextPaint wp = mWorkPaint; 1112 wp.set(mPaint); 1113 wp.setStartHyphenEdit(adjustStartHyphenEdit(start, wp.getStartHyphenEdit())); 1114 wp.setEndHyphenEdit(adjustEndHyphenEdit(limit, wp.getEndHyphenEdit())); 1115 return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top, 1116 y, bottom, fmi, needWidth, measureLimit, null); 1117 } 1118 1119 // Shaping needs to take into account context up to metric boundaries, 1120 // but rendering needs to take into account character style boundaries. 1121 // So we iterate through metric runs to get metric bounds, 1122 // then within each metric run iterate through character style runs 1123 // for the run bounds. 1124 final float originalX = x; 1125 for (int i = start, inext; i < measureLimit; i = inext) { 1126 final TextPaint wp = mWorkPaint; 1127 wp.set(mPaint); 1128 1129 inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) - 1130 mStart; 1131 int mlimit = Math.min(inext, measureLimit); 1132 1133 ReplacementSpan replacement = null; 1134 1135 for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) { 1136 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT 1137 // empty by construction. This special case in getSpans() explains the >= & <= tests 1138 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) 1139 || (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue; 1140 1141 boolean insideEllipsis = 1142 mStart + mEllipsisStart <= mMetricAffectingSpanSpanSet.spanStarts[j] 1143 && mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + mEllipsisEnd; 1144 final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j]; 1145 if (span instanceof ReplacementSpan) { 1146 replacement = !insideEllipsis ? (ReplacementSpan) span : null; 1147 } else { 1148 // We might have a replacement that uses the draw 1149 // state, otherwise measure state would suffice. 1150 span.updateDrawState(wp); 1151 } 1152 } 1153 1154 if (replacement != null) { 1155 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y, 1156 bottom, fmi, needWidth || mlimit < measureLimit); 1157 continue; 1158 } 1159 1160 final TextPaint activePaint = mActivePaint; 1161 activePaint.set(mPaint); 1162 int activeStart = i; 1163 int activeEnd = mlimit; 1164 final DecorationInfo decorationInfo = mDecorationInfo; 1165 mDecorations.clear(); 1166 for (int j = i, jnext; j < mlimit; j = jnext) { 1167 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) - 1168 mStart; 1169 1170 final int offset = Math.min(jnext, mlimit); 1171 wp.set(mPaint); 1172 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) { 1173 // Intentionally using >= and <= as explained above 1174 if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) || 1175 (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue; 1176 1177 final CharacterStyle span = mCharacterStyleSpanSet.spans[k]; 1178 span.updateDrawState(wp); 1179 } 1180 1181 extractDecorationInfo(wp, decorationInfo); 1182 1183 if (j == i) { 1184 // First chunk of text. We can't handle it yet, since we may need to merge it 1185 // with the next chunk. So we just save the TextPaint for future comparisons 1186 // and use. 1187 activePaint.set(wp); 1188 } else if (!equalAttributes(wp, activePaint)) { 1189 // The style of the present chunk of text is substantially different from the 1190 // style of the previous chunk. We need to handle the active piece of text 1191 // and restart with the present chunk. 1192 activePaint.setStartHyphenEdit( 1193 adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit())); 1194 activePaint.setEndHyphenEdit( 1195 adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit())); 1196 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x, 1197 top, y, bottom, fmi, needWidth || activeEnd < measureLimit, 1198 Math.min(activeEnd, mlimit), mDecorations); 1199 1200 activeStart = j; 1201 activePaint.set(wp); 1202 mDecorations.clear(); 1203 } else { 1204 // The present TextPaint is substantially equal to the last TextPaint except 1205 // perhaps for decorations. We just need to expand the active piece of text to 1206 // include the present chunk, which we always do anyway. We don't need to save 1207 // wp to activePaint, since they are already equal. 1208 } 1209 1210 activeEnd = jnext; 1211 if (decorationInfo.hasDecoration()) { 1212 final DecorationInfo copy = decorationInfo.copyInfo(); 1213 copy.start = j; 1214 copy.end = jnext; 1215 mDecorations.add(copy); 1216 } 1217 } 1218 // Handle the final piece of text. 1219 activePaint.setStartHyphenEdit( 1220 adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit())); 1221 activePaint.setEndHyphenEdit( 1222 adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit())); 1223 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x, 1224 top, y, bottom, fmi, needWidth || activeEnd < measureLimit, 1225 Math.min(activeEnd, mlimit), mDecorations); 1226 } 1227 1228 return x - originalX; 1229 } 1230 1231 /** 1232 * Render a text run with the set-up paint. 1233 * 1234 * @param c the canvas 1235 * @param wp the paint used to render the text 1236 * @param start the start of the run 1237 * @param end the end of the run 1238 * @param contextStart the start of context for the run 1239 * @param contextEnd the end of the context for the run 1240 * @param runIsRtl true if the run is right-to-left 1241 * @param x the x position of the left edge of the run 1242 * @param y the baseline of the run 1243 */ drawTextRun(Canvas c, TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x, int y)1244 private void drawTextRun(Canvas c, TextPaint wp, int start, int end, 1245 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) { 1246 1247 if (mCharsValid) { 1248 int count = end - start; 1249 int contextCount = contextEnd - contextStart; 1250 c.drawTextRun(mChars, start, count, contextStart, contextCount, 1251 x, y, runIsRtl, wp); 1252 } else { 1253 int delta = mStart; 1254 c.drawTextRun(mText, delta + start, delta + end, 1255 delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp); 1256 } 1257 } 1258 1259 /** 1260 * Returns the next tab position. 1261 * 1262 * @param h the (unsigned) offset from the leading margin 1263 * @return the (unsigned) tab position after this offset 1264 */ nextTab(float h)1265 float nextTab(float h) { 1266 if (mTabs != null) { 1267 return mTabs.nextTab(h); 1268 } 1269 return TabStops.nextDefaultStop(h, TAB_INCREMENT); 1270 } 1271 isStretchableWhitespace(int ch)1272 private boolean isStretchableWhitespace(int ch) { 1273 // TODO: Support NBSP and other stretchable whitespace (b/34013491 and b/68204709). 1274 return ch == 0x0020; 1275 } 1276 1277 /* Return the number of spaces in the text line, for the purpose of justification */ countStretchableSpaces(int start, int end)1278 private int countStretchableSpaces(int start, int end) { 1279 int count = 0; 1280 for (int i = start; i < end; i++) { 1281 final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart); 1282 if (isStretchableWhitespace(c)) { 1283 count++; 1284 } 1285 } 1286 return count; 1287 } 1288 1289 // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace() isLineEndSpace(char ch)1290 public static boolean isLineEndSpace(char ch) { 1291 return ch == ' ' || ch == '\t' || ch == 0x1680 1292 || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007) 1293 || ch == 0x205F || ch == 0x3000; 1294 } 1295 1296 private static final int TAB_INCREMENT = 20; 1297 equalAttributes(@onNull TextPaint lp, @NonNull TextPaint rp)1298 private static boolean equalAttributes(@NonNull TextPaint lp, @NonNull TextPaint rp) { 1299 return lp.getColorFilter() == rp.getColorFilter() 1300 && lp.getMaskFilter() == rp.getMaskFilter() 1301 && lp.getShader() == rp.getShader() 1302 && lp.getTypeface() == rp.getTypeface() 1303 && lp.getXfermode() == rp.getXfermode() 1304 && lp.getTextLocales().equals(rp.getTextLocales()) 1305 && TextUtils.equals(lp.getFontFeatureSettings(), rp.getFontFeatureSettings()) 1306 && TextUtils.equals(lp.getFontVariationSettings(), rp.getFontVariationSettings()) 1307 && lp.getShadowLayerRadius() == rp.getShadowLayerRadius() 1308 && lp.getShadowLayerDx() == rp.getShadowLayerDx() 1309 && lp.getShadowLayerDy() == rp.getShadowLayerDy() 1310 && lp.getShadowLayerColor() == rp.getShadowLayerColor() 1311 && lp.getFlags() == rp.getFlags() 1312 && lp.getHinting() == rp.getHinting() 1313 && lp.getStyle() == rp.getStyle() 1314 && lp.getColor() == rp.getColor() 1315 && lp.getStrokeWidth() == rp.getStrokeWidth() 1316 && lp.getStrokeMiter() == rp.getStrokeMiter() 1317 && lp.getStrokeCap() == rp.getStrokeCap() 1318 && lp.getStrokeJoin() == rp.getStrokeJoin() 1319 && lp.getTextAlign() == rp.getTextAlign() 1320 && lp.isElegantTextHeight() == rp.isElegantTextHeight() 1321 && lp.getTextSize() == rp.getTextSize() 1322 && lp.getTextScaleX() == rp.getTextScaleX() 1323 && lp.getTextSkewX() == rp.getTextSkewX() 1324 && lp.getLetterSpacing() == rp.getLetterSpacing() 1325 && lp.getWordSpacing() == rp.getWordSpacing() 1326 && lp.getStartHyphenEdit() == rp.getStartHyphenEdit() 1327 && lp.getEndHyphenEdit() == rp.getEndHyphenEdit() 1328 && lp.bgColor == rp.bgColor 1329 && lp.baselineShift == rp.baselineShift 1330 && lp.linkColor == rp.linkColor 1331 && lp.drawableState == rp.drawableState 1332 && lp.density == rp.density 1333 && lp.underlineColor == rp.underlineColor 1334 && lp.underlineThickness == rp.underlineThickness; 1335 } 1336 } 1337