TextLine.java revision fd8c22d513d92d547f883efc45a3dbcd9558f90a
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 drawUnderline(wp, c, wp.getColor(), ((Paint) wp).getUnderlineThickness(), 831 underlineXLeft, underlineXRight, y); 832 } 833 } 834 } 835 836 drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl, 837 leftX, y + wp.baselineShift); 838 } 839 840 return runIsRtl ? -totalWidth : totalWidth; 841 } 842 843 /** 844 * Utility function for measuring and rendering a replacement. 845 * 846 * 847 * @param replacement the replacement 848 * @param wp the work paint 849 * @param start the start of the run 850 * @param limit the limit of the run 851 * @param runIsRtl true if the run is right-to-left 852 * @param c the canvas, can be null if not rendering 853 * @param x the edge of the replacement closest to the leading margin 854 * @param top the top of the line 855 * @param y the baseline 856 * @param bottom the bottom of the line 857 * @param fmi receives metrics information, can be null 858 * @param needWidth true if the width of the replacement is needed 859 * @return the signed width of the run based on the run direction; only 860 * valid if needWidth is true 861 */ 862 private float handleReplacement(ReplacementSpan replacement, TextPaint wp, 863 int start, int limit, boolean runIsRtl, Canvas c, 864 float x, int top, int y, int bottom, FontMetricsInt fmi, 865 boolean needWidth) { 866 867 float ret = 0; 868 869 int textStart = mStart + start; 870 int textLimit = mStart + limit; 871 872 if (needWidth || (c != null && runIsRtl)) { 873 int previousTop = 0; 874 int previousAscent = 0; 875 int previousDescent = 0; 876 int previousBottom = 0; 877 int previousLeading = 0; 878 879 boolean needUpdateMetrics = (fmi != null); 880 881 if (needUpdateMetrics) { 882 previousTop = fmi.top; 883 previousAscent = fmi.ascent; 884 previousDescent = fmi.descent; 885 previousBottom = fmi.bottom; 886 previousLeading = fmi.leading; 887 } 888 889 ret = replacement.getSize(wp, mText, textStart, textLimit, fmi); 890 891 if (needUpdateMetrics) { 892 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom, 893 previousLeading); 894 } 895 } 896 897 if (c != null) { 898 if (runIsRtl) { 899 x -= ret; 900 } 901 replacement.draw(c, mText, textStart, textLimit, 902 x, top, y, bottom, wp); 903 } 904 905 return runIsRtl ? -ret : ret; 906 } 907 908 private int adjustHyphenEdit(int start, int limit, int hyphenEdit) { 909 int result = hyphenEdit; 910 // Only draw hyphens on first or last run in line. Disable them otherwise. 911 if (start > 0) { // not the first run 912 result &= ~Paint.HYPHENEDIT_MASK_START_OF_LINE; 913 } 914 if (limit < mLen) { // not the last run 915 result &= ~Paint.HYPHENEDIT_MASK_END_OF_LINE; 916 } 917 return result; 918 } 919 920 private static final class UnderlineInfo { 921 public boolean isUnderlineText; 922 public int underlineColor; 923 public float underlineThickness; 924 public int start = -1; 925 public int end = -1; 926 927 public boolean hasUnderline() { 928 return isUnderlineText || underlineColor != 0; 929 } 930 931 // Copies the info, but not the start and end range. 932 public UnderlineInfo copyInfo() { 933 final UnderlineInfo copy = new UnderlineInfo(); 934 copy.isUnderlineText = isUnderlineText; 935 copy.underlineColor = underlineColor; 936 copy.underlineThickness = underlineThickness; 937 return copy; 938 } 939 } 940 941 private void extractUnderlineInfo(@NonNull TextPaint paint, @NonNull UnderlineInfo info) { 942 info.isUnderlineText = paint.isUnderlineText(); 943 if (info.isUnderlineText) { 944 paint.setUnderlineText(false); 945 } 946 info.underlineColor = paint.underlineColor; 947 info.underlineThickness = paint.underlineThickness; 948 paint.setUnderlineText(0, 0.0f); 949 } 950 951 /** 952 * Utility function for handling a unidirectional run. The run must not 953 * contain tabs but can contain styles. 954 * 955 * 956 * @param start the line-relative start of the run 957 * @param measureLimit the offset to measure to, between start and limit inclusive 958 * @param limit the limit of the run 959 * @param runIsRtl true if the run is right-to-left 960 * @param c the canvas, can be null 961 * @param x the end of the run closest to the leading margin 962 * @param top the top of the line 963 * @param y the baseline 964 * @param bottom the bottom of the line 965 * @param fmi receives metrics information, can be null 966 * @param needWidth true if the width is required 967 * @return the signed width of the run based on the run direction; only 968 * valid if needWidth is true 969 */ 970 private float handleRun(int start, int measureLimit, 971 int limit, boolean runIsRtl, Canvas c, float x, int top, int y, 972 int bottom, FontMetricsInt fmi, boolean needWidth) { 973 974 if (measureLimit < start || measureLimit > limit) { 975 throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of " 976 + "start (" + start + ") and limit (" + limit + ") bounds"); 977 } 978 979 // Case of an empty line, make sure we update fmi according to mPaint 980 if (start == measureLimit) { 981 final TextPaint wp = mWorkPaint; 982 wp.set(mPaint); 983 if (fmi != null) { 984 expandMetricsFromPaint(fmi, wp); 985 } 986 return 0f; 987 } 988 989 if (mSpanned == null) { 990 final TextPaint wp = mWorkPaint; 991 wp.set(mPaint); 992 wp.setHyphenEdit(adjustHyphenEdit(start, limit, wp.getHyphenEdit())); 993 return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top, 994 y, bottom, fmi, needWidth, measureLimit, null); 995 } 996 997 mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit); 998 mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit); 999 1000 // Shaping needs to take into account context up to metric boundaries, 1001 // but rendering needs to take into account character style boundaries. 1002 // So we iterate through metric runs to get metric bounds, 1003 // then within each metric run iterate through character style runs 1004 // for the run bounds. 1005 final float originalX = x; 1006 for (int i = start, inext; i < measureLimit; i = inext) { 1007 final TextPaint wp = mWorkPaint; 1008 wp.set(mPaint); 1009 1010 inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) - 1011 mStart; 1012 int mlimit = Math.min(inext, measureLimit); 1013 1014 ReplacementSpan replacement = null; 1015 1016 for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) { 1017 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT 1018 // empty by construction. This special case in getSpans() explains the >= & <= tests 1019 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) || 1020 (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue; 1021 final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j]; 1022 if (span instanceof ReplacementSpan) { 1023 replacement = (ReplacementSpan)span; 1024 } else { 1025 // We might have a replacement that uses the draw 1026 // state, otherwise measure state would suffice. 1027 span.updateDrawState(wp); 1028 } 1029 } 1030 1031 if (replacement != null) { 1032 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y, 1033 bottom, fmi, needWidth || mlimit < measureLimit); 1034 continue; 1035 } 1036 1037 final TextPaint activePaint = mActivePaint; 1038 activePaint.set(mPaint); 1039 int activeStart = i; 1040 int activeEnd = mlimit; 1041 final UnderlineInfo underlineInfo = mUnderlineInfo; 1042 mUnderlines.clear(); 1043 for (int j = i, jnext; j < mlimit; j = jnext) { 1044 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) - 1045 mStart; 1046 1047 final int offset = Math.min(jnext, mlimit); 1048 wp.set(mPaint); 1049 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) { 1050 // Intentionally using >= and <= as explained above 1051 if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) || 1052 (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue; 1053 1054 final CharacterStyle span = mCharacterStyleSpanSet.spans[k]; 1055 span.updateDrawState(wp); 1056 } 1057 1058 extractUnderlineInfo(wp, underlineInfo); 1059 1060 if (j == i) { 1061 // First chunk of text. We can't handle it yet, since we may need to merge it 1062 // with the next chunk. So we just save the TextPaint for future comparisons 1063 // and use. 1064 activePaint.set(wp); 1065 } else if (!wp.hasEqualAttributes(activePaint)) { 1066 // The style of the present chunk of text is substantially different from the 1067 // style of the previous chunk. We need to handle the active piece of text 1068 // and restart with the present chunk. 1069 activePaint.setHyphenEdit(adjustHyphenEdit( 1070 activeStart, activeEnd, mPaint.getHyphenEdit())); 1071 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x, 1072 top, y, bottom, fmi, needWidth || activeEnd < measureLimit, 1073 Math.min(activeEnd, mlimit), mUnderlines); 1074 1075 activeStart = j; 1076 activePaint.set(wp); 1077 mUnderlines.clear(); 1078 } else { 1079 // The present TextPaint is substantially equal to the last TextPaint except 1080 // perhaps for underlines. We just need to expand the active piece of text to 1081 // include the present chunk, which we always do anyway. We don't need to save 1082 // wp to activePaint, since they are already equal. 1083 } 1084 1085 activeEnd = jnext; 1086 if (underlineInfo.hasUnderline()) { 1087 final UnderlineInfo copy = underlineInfo.copyInfo(); 1088 copy.start = j; 1089 copy.end = jnext; 1090 mUnderlines.add(copy); 1091 } 1092 } 1093 // Handle the final piece of text. 1094 activePaint.setHyphenEdit(adjustHyphenEdit( 1095 activeStart, activeEnd, mPaint.getHyphenEdit())); 1096 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x, 1097 top, y, bottom, fmi, needWidth || activeEnd < measureLimit, 1098 Math.min(activeEnd, mlimit), mUnderlines); 1099 } 1100 1101 return x - originalX; 1102 } 1103 1104 /** 1105 * Render a text run with the set-up paint. 1106 * 1107 * @param c the canvas 1108 * @param wp the paint used to render the text 1109 * @param start the start of the run 1110 * @param end the end of the run 1111 * @param contextStart the start of context for the run 1112 * @param contextEnd the end of the context for the run 1113 * @param runIsRtl true if the run is right-to-left 1114 * @param x the x position of the left edge of the run 1115 * @param y the baseline of the run 1116 */ 1117 private void drawTextRun(Canvas c, TextPaint wp, int start, int end, 1118 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) { 1119 1120 if (mCharsValid) { 1121 int count = end - start; 1122 int contextCount = contextEnd - contextStart; 1123 c.drawTextRun(mChars, start, count, contextStart, contextCount, 1124 x, y, runIsRtl, wp); 1125 } else { 1126 int delta = mStart; 1127 c.drawTextRun(mText, delta + start, delta + end, 1128 delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp); 1129 } 1130 } 1131 1132 /** 1133 * Returns the next tab position. 1134 * 1135 * @param h the (unsigned) offset from the leading margin 1136 * @return the (unsigned) tab position after this offset 1137 */ 1138 float nextTab(float h) { 1139 if (mTabs != null) { 1140 return mTabs.nextTab(h); 1141 } 1142 return TabStops.nextDefaultStop(h, TAB_INCREMENT); 1143 } 1144 1145 private boolean isStretchableWhitespace(int ch) { 1146 // TODO: Support other stretchable whitespace. (Bug: 34013491) 1147 return ch == 0x0020 || ch == 0x00A0; 1148 } 1149 1150 private int nextStretchableSpace(int start, int end) { 1151 for (int i = start; i < end; i++) { 1152 final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart); 1153 if (isStretchableWhitespace(c)) return i; 1154 } 1155 return end; 1156 } 1157 1158 /* Return the number of spaces in the text line, for the purpose of justification */ 1159 private int countStretchableSpaces(int start, int end) { 1160 int count = 0; 1161 for (int i = start; i < end; i = nextStretchableSpace(i + 1, end)) { 1162 count++; 1163 } 1164 return count; 1165 } 1166 1167 // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace() 1168 public static boolean isLineEndSpace(char ch) { 1169 return ch == ' ' || ch == '\t' || ch == 0x1680 1170 || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007) 1171 || ch == 0x205F || ch == 0x3000; 1172 } 1173 1174 private static final int TAB_INCREMENT = 20; 1175} 1176