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