TextLine.java revision ca8a04a36640eb227a556ad9ced925c48ced2495
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, float baseline) { 709 final float underlineTop = baseline + wp.baselineShift + wp.getUnderlinePosition(); 710 711 final int previousColor = wp.getColor(); 712 final Paint.Style previousStyle = wp.getStyle(); 713 final boolean previousAntiAlias = wp.isAntiAlias(); 714 715 wp.setStyle(Paint.Style.FILL); 716 wp.setAntiAlias(true); 717 718 wp.setColor(color); 719 c.drawRect(xleft, underlineTop, xright, underlineTop + thickness, wp); 720 721 wp.setStyle(previousStyle); 722 wp.setColor(previousColor); 723 wp.setAntiAlias(previousAntiAlias); 724 } 725 726 private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd, 727 boolean runIsRtl, int offset) { 728 if (mCharsValid) { 729 return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset); 730 } else { 731 final int delta = mStart; 732 return wp.getRunAdvance(mText, delta + start, delta + end, 733 delta + contextStart, delta + contextEnd, runIsRtl, delta + offset); 734 } 735 } 736 737 /** 738 * Utility function for measuring and rendering text. The text must 739 * not include a tab. 740 * 741 * @param wp the working paint 742 * @param start the start of the text 743 * @param end the end of the text 744 * @param runIsRtl true if the run is right-to-left 745 * @param c the canvas, can be null if rendering is not needed 746 * @param x the edge of the run closest to the leading margin 747 * @param top the top of the line 748 * @param y the baseline 749 * @param bottom the bottom of the line 750 * @param fmi receives metrics information, can be null 751 * @param needWidth true if the width of the run is needed 752 * @param offset the offset for the purpose of measuring 753 * @param underlines the list of locations and paremeters for drawing underlines 754 * @return the signed width of the run based on the run direction; only 755 * valid if needWidth is true 756 */ 757 private float handleText(TextPaint wp, int start, int end, 758 int contextStart, int contextEnd, boolean runIsRtl, 759 Canvas c, float x, int top, int y, int bottom, 760 FontMetricsInt fmi, boolean needWidth, int offset, 761 @Nullable ArrayList<UnderlineInfo> underlines) { 762 763 wp.setWordSpacing(mAddedWidth); 764 // Get metrics first (even for empty strings or "0" width runs) 765 if (fmi != null) { 766 expandMetricsFromPaint(fmi, wp); 767 } 768 769 // No need to do anything if the run width is "0" 770 if (end == start) { 771 return 0f; 772 } 773 774 float totalWidth = 0; 775 776 final int numUnderlines = underlines == null ? 0 : underlines.size(); 777 if (needWidth || (c != null && (wp.bgColor != 0 || numUnderlines != 0 || runIsRtl))) { 778 totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset); 779 } 780 781 if (c != null) { 782 final float leftX, rightX; 783 if (runIsRtl) { 784 leftX = x - totalWidth; 785 rightX = x; 786 } else { 787 leftX = x; 788 rightX = x + totalWidth; 789 } 790 791 if (wp.bgColor != 0) { 792 int previousColor = wp.getColor(); 793 Paint.Style previousStyle = wp.getStyle(); 794 795 wp.setColor(wp.bgColor); 796 wp.setStyle(Paint.Style.FILL); 797 c.drawRect(leftX, top, rightX, bottom, wp); 798 799 wp.setStyle(previousStyle); 800 wp.setColor(previousColor); 801 } 802 803 if (numUnderlines != 0) { 804 for (int i = 0; i < numUnderlines; i++) { 805 final UnderlineInfo info = underlines.get(i); 806 807 final int underlineStart = Math.max(info.start, start); 808 final int underlineEnd = Math.min(info.end, offset); 809 float underlineStartAdvance = getRunAdvance( 810 wp, start, end, contextStart, contextEnd, runIsRtl, underlineStart); 811 float underlineEndAdvance = getRunAdvance( 812 wp, start, end, contextStart, contextEnd, runIsRtl, underlineEnd); 813 final float underlineXLeft, underlineXRight; 814 if (runIsRtl) { 815 underlineXLeft = rightX - underlineEndAdvance; 816 underlineXRight = rightX - underlineStartAdvance; 817 } else { 818 underlineXLeft = leftX + underlineStartAdvance; 819 underlineXRight = leftX + underlineEndAdvance; 820 } 821 822 // Theoretically, there could be cases where both Paint's and TextPaint's 823 // setUnderLineText() are called. For backward compatibility, we need to draw 824 // both underlines, the one with custom color first. 825 if (info.underlineColor != 0) { 826 drawUnderline(wp, c, info.underlineColor, info.underlineThickness, 827 underlineXLeft, underlineXRight, y); 828 } 829 if (info.isUnderlineText) { 830 final float thickness = 831 Math.max(((Paint) wp).getUnderlineThickness(), 1.0f); 832 drawUnderline(wp, c, wp.getColor(), thickness, 833 underlineXLeft, underlineXRight, y); 834 } 835 } 836 } 837 838 drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl, 839 leftX, y + wp.baselineShift); 840 } 841 842 return runIsRtl ? -totalWidth : totalWidth; 843 } 844 845 /** 846 * Utility function for measuring and rendering a replacement. 847 * 848 * 849 * @param replacement the replacement 850 * @param wp the work paint 851 * @param start the start of the run 852 * @param limit the limit of the run 853 * @param runIsRtl true if the run is right-to-left 854 * @param c the canvas, can be null if not rendering 855 * @param x the edge of the replacement closest to the leading margin 856 * @param top the top of the line 857 * @param y the baseline 858 * @param bottom the bottom of the line 859 * @param fmi receives metrics information, can be null 860 * @param needWidth true if the width of the replacement is needed 861 * @return the signed width of the run based on the run direction; only 862 * valid if needWidth is true 863 */ 864 private float handleReplacement(ReplacementSpan replacement, TextPaint wp, 865 int start, int limit, boolean runIsRtl, Canvas c, 866 float x, int top, int y, int bottom, FontMetricsInt fmi, 867 boolean needWidth) { 868 869 float ret = 0; 870 871 int textStart = mStart + start; 872 int textLimit = mStart + limit; 873 874 if (needWidth || (c != null && runIsRtl)) { 875 int previousTop = 0; 876 int previousAscent = 0; 877 int previousDescent = 0; 878 int previousBottom = 0; 879 int previousLeading = 0; 880 881 boolean needUpdateMetrics = (fmi != null); 882 883 if (needUpdateMetrics) { 884 previousTop = fmi.top; 885 previousAscent = fmi.ascent; 886 previousDescent = fmi.descent; 887 previousBottom = fmi.bottom; 888 previousLeading = fmi.leading; 889 } 890 891 ret = replacement.getSize(wp, mText, textStart, textLimit, fmi); 892 893 if (needUpdateMetrics) { 894 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom, 895 previousLeading); 896 } 897 } 898 899 if (c != null) { 900 if (runIsRtl) { 901 x -= ret; 902 } 903 replacement.draw(c, mText, textStart, textLimit, 904 x, top, y, bottom, wp); 905 } 906 907 return runIsRtl ? -ret : ret; 908 } 909 910 private int adjustHyphenEdit(int start, int limit, int hyphenEdit) { 911 int result = hyphenEdit; 912 // Only draw hyphens on first or last run in line. Disable them otherwise. 913 if (start > 0) { // not the first run 914 result &= ~Paint.HYPHENEDIT_MASK_START_OF_LINE; 915 } 916 if (limit < mLen) { // not the last run 917 result &= ~Paint.HYPHENEDIT_MASK_END_OF_LINE; 918 } 919 return result; 920 } 921 922 private static final class UnderlineInfo { 923 public boolean isUnderlineText; 924 public int underlineColor; 925 public float underlineThickness; 926 public int start = -1; 927 public int end = -1; 928 929 public boolean hasUnderline() { 930 return isUnderlineText || underlineColor != 0; 931 } 932 933 // Copies the info, but not the start and end range. 934 public UnderlineInfo copyInfo() { 935 final UnderlineInfo copy = new UnderlineInfo(); 936 copy.isUnderlineText = isUnderlineText; 937 copy.underlineColor = underlineColor; 938 copy.underlineThickness = underlineThickness; 939 return copy; 940 } 941 } 942 943 private void extractUnderlineInfo(@NonNull TextPaint paint, @NonNull UnderlineInfo info) { 944 info.isUnderlineText = paint.isUnderlineText(); 945 if (info.isUnderlineText) { 946 paint.setUnderlineText(false); 947 } 948 info.underlineColor = paint.underlineColor; 949 info.underlineThickness = paint.underlineThickness; 950 paint.setUnderlineText(0, 0.0f); 951 } 952 953 /** 954 * Utility function for handling a unidirectional run. The run must not 955 * contain tabs but can contain styles. 956 * 957 * 958 * @param start the line-relative start of the run 959 * @param measureLimit the offset to measure to, between start and limit inclusive 960 * @param limit the limit of the run 961 * @param runIsRtl true if the run is right-to-left 962 * @param c the canvas, can be null 963 * @param x the end of the run closest to the leading margin 964 * @param top the top of the line 965 * @param y the baseline 966 * @param bottom the bottom of the line 967 * @param fmi receives metrics information, can be null 968 * @param needWidth true if the width is required 969 * @return the signed width of the run based on the run direction; only 970 * valid if needWidth is true 971 */ 972 private float handleRun(int start, int measureLimit, 973 int limit, boolean runIsRtl, Canvas c, float x, int top, int y, 974 int bottom, FontMetricsInt fmi, boolean needWidth) { 975 976 if (measureLimit < start || measureLimit > limit) { 977 throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of " 978 + "start (" + start + ") and limit (" + limit + ") bounds"); 979 } 980 981 // Case of an empty line, make sure we update fmi according to mPaint 982 if (start == measureLimit) { 983 final TextPaint wp = mWorkPaint; 984 wp.set(mPaint); 985 if (fmi != null) { 986 expandMetricsFromPaint(fmi, wp); 987 } 988 return 0f; 989 } 990 991 if (mSpanned == null) { 992 final TextPaint wp = mWorkPaint; 993 wp.set(mPaint); 994 wp.setHyphenEdit(adjustHyphenEdit(start, limit, wp.getHyphenEdit())); 995 return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top, 996 y, bottom, fmi, needWidth, measureLimit, null); 997 } 998 999 mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit); 1000 mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit); 1001 1002 // Shaping needs to take into account context up to metric boundaries, 1003 // but rendering needs to take into account character style boundaries. 1004 // So we iterate through metric runs to get metric bounds, 1005 // then within each metric run iterate through character style runs 1006 // for the run bounds. 1007 final float originalX = x; 1008 for (int i = start, inext; i < measureLimit; i = inext) { 1009 final TextPaint wp = mWorkPaint; 1010 wp.set(mPaint); 1011 1012 inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) - 1013 mStart; 1014 int mlimit = Math.min(inext, measureLimit); 1015 1016 ReplacementSpan replacement = null; 1017 1018 for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) { 1019 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT 1020 // empty by construction. This special case in getSpans() explains the >= & <= tests 1021 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) || 1022 (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue; 1023 final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j]; 1024 if (span instanceof ReplacementSpan) { 1025 replacement = (ReplacementSpan)span; 1026 } else { 1027 // We might have a replacement that uses the draw 1028 // state, otherwise measure state would suffice. 1029 span.updateDrawState(wp); 1030 } 1031 } 1032 1033 if (replacement != null) { 1034 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y, 1035 bottom, fmi, needWidth || mlimit < measureLimit); 1036 continue; 1037 } 1038 1039 final TextPaint activePaint = mActivePaint; 1040 activePaint.set(mPaint); 1041 int activeStart = i; 1042 int activeEnd = mlimit; 1043 final UnderlineInfo underlineInfo = mUnderlineInfo; 1044 mUnderlines.clear(); 1045 for (int j = i, jnext; j < mlimit; j = jnext) { 1046 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) - 1047 mStart; 1048 1049 final int offset = Math.min(jnext, mlimit); 1050 wp.set(mPaint); 1051 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) { 1052 // Intentionally using >= and <= as explained above 1053 if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) || 1054 (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue; 1055 1056 final CharacterStyle span = mCharacterStyleSpanSet.spans[k]; 1057 span.updateDrawState(wp); 1058 } 1059 1060 extractUnderlineInfo(wp, underlineInfo); 1061 1062 if (j == i) { 1063 // First chunk of text. We can't handle it yet, since we may need to merge it 1064 // with the next chunk. So we just save the TextPaint for future comparisons 1065 // and use. 1066 activePaint.set(wp); 1067 } else if (!wp.hasEqualAttributes(activePaint)) { 1068 // The style of the present chunk of text is substantially different from the 1069 // style of the previous chunk. We need to handle the active piece of text 1070 // and restart with the present chunk. 1071 activePaint.setHyphenEdit(adjustHyphenEdit( 1072 activeStart, activeEnd, mPaint.getHyphenEdit())); 1073 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x, 1074 top, y, bottom, fmi, needWidth || activeEnd < measureLimit, 1075 Math.min(activeEnd, mlimit), mUnderlines); 1076 1077 activeStart = j; 1078 activePaint.set(wp); 1079 mUnderlines.clear(); 1080 } else { 1081 // The present TextPaint is substantially equal to the last TextPaint except 1082 // perhaps for underlines. We just need to expand the active piece of text to 1083 // include the present chunk, which we always do anyway. We don't need to save 1084 // wp to activePaint, since they are already equal. 1085 } 1086 1087 activeEnd = jnext; 1088 if (underlineInfo.hasUnderline()) { 1089 final UnderlineInfo copy = underlineInfo.copyInfo(); 1090 copy.start = j; 1091 copy.end = jnext; 1092 mUnderlines.add(copy); 1093 } 1094 } 1095 // Handle the final piece of text. 1096 activePaint.setHyphenEdit(adjustHyphenEdit( 1097 activeStart, activeEnd, mPaint.getHyphenEdit())); 1098 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x, 1099 top, y, bottom, fmi, needWidth || activeEnd < measureLimit, 1100 Math.min(activeEnd, mlimit), mUnderlines); 1101 } 1102 1103 return x - originalX; 1104 } 1105 1106 /** 1107 * Render a text run with the set-up paint. 1108 * 1109 * @param c the canvas 1110 * @param wp the paint used to render the text 1111 * @param start the start of the run 1112 * @param end the end of the run 1113 * @param contextStart the start of context for the run 1114 * @param contextEnd the end of the context for the run 1115 * @param runIsRtl true if the run is right-to-left 1116 * @param x the x position of the left edge of the run 1117 * @param y the baseline of the run 1118 */ 1119 private void drawTextRun(Canvas c, TextPaint wp, int start, int end, 1120 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) { 1121 1122 if (mCharsValid) { 1123 int count = end - start; 1124 int contextCount = contextEnd - contextStart; 1125 c.drawTextRun(mChars, start, count, contextStart, contextCount, 1126 x, y, runIsRtl, wp); 1127 } else { 1128 int delta = mStart; 1129 c.drawTextRun(mText, delta + start, delta + end, 1130 delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp); 1131 } 1132 } 1133 1134 /** 1135 * Returns the next tab position. 1136 * 1137 * @param h the (unsigned) offset from the leading margin 1138 * @return the (unsigned) tab position after this offset 1139 */ 1140 float nextTab(float h) { 1141 if (mTabs != null) { 1142 return mTabs.nextTab(h); 1143 } 1144 return TabStops.nextDefaultStop(h, TAB_INCREMENT); 1145 } 1146 1147 private boolean isStretchableWhitespace(int ch) { 1148 // TODO: Support other stretchable whitespace. (Bug: 34013491) 1149 return ch == 0x0020 || ch == 0x00A0; 1150 } 1151 1152 private int nextStretchableSpace(int start, int end) { 1153 for (int i = start; i < end; i++) { 1154 final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart); 1155 if (isStretchableWhitespace(c)) return i; 1156 } 1157 return end; 1158 } 1159 1160 /* Return the number of spaces in the text line, for the purpose of justification */ 1161 private int countStretchableSpaces(int start, int end) { 1162 int count = 0; 1163 for (int i = start; i < end; i = nextStretchableSpace(i + 1, end)) { 1164 count++; 1165 } 1166 return count; 1167 } 1168 1169 // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace() 1170 public static boolean isLineEndSpace(char ch) { 1171 return ch == ' ' || ch == '\t' || ch == 0x1680 1172 || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007) 1173 || ch == 0x205F || ch == 0x3000; 1174 } 1175 1176 private static final int TAB_INCREMENT = 20; 1177} 1178