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