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