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