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