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