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