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