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