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