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