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