TextLine.java revision 554585e08da5e89762105b2adc0b4c76651d1d68
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 final boolean needsSpanMeasurement; 992 if (mSpanned == null) { 993 needsSpanMeasurement = false; 994 } else { 995 mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit); 996 mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit); 997 needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0 998 || mCharacterStyleSpanSet.numberOfSpans != 0; 999 } 1000 1001 if (!needsSpanMeasurement) { 1002 final TextPaint wp = mWorkPaint; 1003 wp.set(mPaint); 1004 wp.setHyphenEdit(adjustHyphenEdit(start, limit, wp.getHyphenEdit())); 1005 return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top, 1006 y, bottom, fmi, needWidth, measureLimit, null); 1007 } 1008 1009 // Shaping needs to take into account context up to metric boundaries, 1010 // but rendering needs to take into account character style boundaries. 1011 // So we iterate through metric runs to get metric bounds, 1012 // then within each metric run iterate through character style runs 1013 // for the run bounds. 1014 final float originalX = x; 1015 for (int i = start, inext; i < measureLimit; i = inext) { 1016 final TextPaint wp = mWorkPaint; 1017 wp.set(mPaint); 1018 1019 inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) - 1020 mStart; 1021 int mlimit = Math.min(inext, measureLimit); 1022 1023 ReplacementSpan replacement = null; 1024 1025 for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) { 1026 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT 1027 // empty by construction. This special case in getSpans() explains the >= & <= tests 1028 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) || 1029 (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue; 1030 final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j]; 1031 if (span instanceof ReplacementSpan) { 1032 replacement = (ReplacementSpan)span; 1033 } else { 1034 // We might have a replacement that uses the draw 1035 // state, otherwise measure state would suffice. 1036 span.updateDrawState(wp); 1037 } 1038 } 1039 1040 if (replacement != null) { 1041 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y, 1042 bottom, fmi, needWidth || mlimit < measureLimit); 1043 continue; 1044 } 1045 1046 final TextPaint activePaint = mActivePaint; 1047 activePaint.set(mPaint); 1048 int activeStart = i; 1049 int activeEnd = mlimit; 1050 final UnderlineInfo underlineInfo = mUnderlineInfo; 1051 mUnderlines.clear(); 1052 for (int j = i, jnext; j < mlimit; j = jnext) { 1053 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) - 1054 mStart; 1055 1056 final int offset = Math.min(jnext, mlimit); 1057 wp.set(mPaint); 1058 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) { 1059 // Intentionally using >= and <= as explained above 1060 if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) || 1061 (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue; 1062 1063 final CharacterStyle span = mCharacterStyleSpanSet.spans[k]; 1064 span.updateDrawState(wp); 1065 } 1066 1067 extractUnderlineInfo(wp, underlineInfo); 1068 1069 if (j == i) { 1070 // First chunk of text. We can't handle it yet, since we may need to merge it 1071 // with the next chunk. So we just save the TextPaint for future comparisons 1072 // and use. 1073 activePaint.set(wp); 1074 } else if (!wp.hasEqualAttributes(activePaint)) { 1075 // The style of the present chunk of text is substantially different from the 1076 // style of the previous chunk. We need to handle the active piece of text 1077 // and restart with the present chunk. 1078 activePaint.setHyphenEdit(adjustHyphenEdit( 1079 activeStart, activeEnd, mPaint.getHyphenEdit())); 1080 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x, 1081 top, y, bottom, fmi, needWidth || activeEnd < measureLimit, 1082 Math.min(activeEnd, mlimit), mUnderlines); 1083 1084 activeStart = j; 1085 activePaint.set(wp); 1086 mUnderlines.clear(); 1087 } else { 1088 // The present TextPaint is substantially equal to the last TextPaint except 1089 // perhaps for underlines. We just need to expand the active piece of text to 1090 // include the present chunk, which we always do anyway. We don't need to save 1091 // wp to activePaint, since they are already equal. 1092 } 1093 1094 activeEnd = jnext; 1095 if (underlineInfo.hasUnderline()) { 1096 final UnderlineInfo copy = underlineInfo.copyInfo(); 1097 copy.start = j; 1098 copy.end = jnext; 1099 mUnderlines.add(copy); 1100 } 1101 } 1102 // Handle the final piece of text. 1103 activePaint.setHyphenEdit(adjustHyphenEdit( 1104 activeStart, activeEnd, mPaint.getHyphenEdit())); 1105 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x, 1106 top, y, bottom, fmi, needWidth || activeEnd < measureLimit, 1107 Math.min(activeEnd, mlimit), mUnderlines); 1108 } 1109 1110 return x - originalX; 1111 } 1112 1113 /** 1114 * Render a text run with the set-up paint. 1115 * 1116 * @param c the canvas 1117 * @param wp the paint used to render the text 1118 * @param start the start of the run 1119 * @param end the end of the run 1120 * @param contextStart the start of context for the run 1121 * @param contextEnd the end of the context for the run 1122 * @param runIsRtl true if the run is right-to-left 1123 * @param x the x position of the left edge of the run 1124 * @param y the baseline of the run 1125 */ 1126 private void drawTextRun(Canvas c, TextPaint wp, int start, int end, 1127 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) { 1128 1129 if (mCharsValid) { 1130 int count = end - start; 1131 int contextCount = contextEnd - contextStart; 1132 c.drawTextRun(mChars, start, count, contextStart, contextCount, 1133 x, y, runIsRtl, wp); 1134 } else { 1135 int delta = mStart; 1136 c.drawTextRun(mText, delta + start, delta + end, 1137 delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp); 1138 } 1139 } 1140 1141 /** 1142 * Returns the next tab position. 1143 * 1144 * @param h the (unsigned) offset from the leading margin 1145 * @return the (unsigned) tab position after this offset 1146 */ 1147 float nextTab(float h) { 1148 if (mTabs != null) { 1149 return mTabs.nextTab(h); 1150 } 1151 return TabStops.nextDefaultStop(h, TAB_INCREMENT); 1152 } 1153 1154 private boolean isStretchableWhitespace(int ch) { 1155 // TODO: Support other stretchable whitespace. (Bug: 34013491) 1156 return ch == 0x0020 || ch == 0x00A0; 1157 } 1158 1159 private int nextStretchableSpace(int start, int end) { 1160 for (int i = start; i < end; i++) { 1161 final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart); 1162 if (isStretchableWhitespace(c)) return i; 1163 } 1164 return end; 1165 } 1166 1167 /* Return the number of spaces in the text line, for the purpose of justification */ 1168 private int countStretchableSpaces(int start, int end) { 1169 int count = 0; 1170 for (int i = start; i < end; i = nextStretchableSpace(i + 1, end)) { 1171 count++; 1172 } 1173 return count; 1174 } 1175 1176 // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace() 1177 public static boolean isLineEndSpace(char ch) { 1178 return ch == ' ' || ch == '\t' || ch == 0x1680 1179 || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007) 1180 || ch == 0x205F || ch == 0x3000; 1181 } 1182 1183 private static final int TAB_INCREMENT = 20; 1184} 1185