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