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