TextLine.java revision 61ccc6a203e1b0fb235a4eed2b40e0c080f57f0c
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 // Get metrics first (even for empty strings or "0" width runs) 710 if (fmi != null) { 711 expandMetricsFromPaint(fmi, wp); 712 } 713 714 int runLen = end - start; 715 // No need to do anything if the run width is "0" 716 if (runLen == 0) { 717 return 0f; 718 } 719 720 float ret = 0; 721 722 int contextLen = contextEnd - contextStart; 723 if (needWidth || (c != null && (wp.bgColor != 0 || runIsRtl))) { 724 int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR; 725 if (mCharsValid) { 726 ret = wp.getTextRunAdvances(mChars, start, runLen, 727 contextStart, contextLen, flags, null, 0); 728 } else { 729 int delta = mStart; 730 ret = wp.getTextRunAdvances(mText, delta + start, 731 delta + end, delta + contextStart, delta + contextEnd, 732 flags, null, 0); 733 } 734 } 735 736 if (c != null) { 737 if (runIsRtl) { 738 x -= ret; 739 } 740 741 if (wp.bgColor != 0) { 742 int color = wp.getColor(); 743 Paint.Style s = wp.getStyle(); 744 wp.setColor(wp.bgColor); 745 wp.setStyle(Paint.Style.FILL); 746 747 c.drawRect(x, top, x + ret, bottom, wp); 748 749 wp.setStyle(s); 750 wp.setColor(color); 751 } 752 753 drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl, 754 x, y + wp.baselineShift); 755 } 756 757 return runIsRtl ? -ret : ret; 758 } 759 760 /** 761 * Utility function for measuring and rendering a replacement. 762 * 763 * 764 * @param replacement the replacement 765 * @param wp the work paint 766 * @param start the start of the run 767 * @param limit the limit of the run 768 * @param runIsRtl true if the run is right-to-left 769 * @param c the canvas, can be null if not rendering 770 * @param x the edge of the replacement closest to the leading margin 771 * @param top the top of the line 772 * @param y the baseline 773 * @param bottom the bottom of the line 774 * @param fmi receives metrics information, can be null 775 * @param needWidth true if the width of the replacement is needed 776 * @return the signed width of the run based on the run direction; only 777 * valid if needWidth is true 778 */ 779 private float handleReplacement(ReplacementSpan replacement, TextPaint wp, 780 int start, int limit, boolean runIsRtl, Canvas c, 781 float x, int top, int y, int bottom, FontMetricsInt fmi, 782 boolean needWidth) { 783 784 float ret = 0; 785 786 int textStart = mStart + start; 787 int textLimit = mStart + limit; 788 789 if (needWidth || (c != null && runIsRtl)) { 790 ret = replacement.getSize(wp, mText, textStart, textLimit, fmi); 791 } 792 793 if (c != null) { 794 if (runIsRtl) { 795 x -= ret; 796 } 797 replacement.draw(c, mText, textStart, textLimit, 798 x, top, y, bottom, wp); 799 } 800 801 return runIsRtl ? -ret : ret; 802 } 803 804 /** 805 * Utility function for handling a unidirectional run. The run must not 806 * contain tabs or emoji but can contain styles. 807 * 808 * 809 * @param start the line-relative start of the run 810 * @param measureLimit the offset to measure to, between start and limit inclusive 811 * @param limit the limit of the run 812 * @param runIsRtl true if the run is right-to-left 813 * @param c the canvas, can be null 814 * @param x the end of the run closest to the leading margin 815 * @param top the top of the line 816 * @param y the baseline 817 * @param bottom the bottom of the line 818 * @param fmi receives metrics information, can be null 819 * @param needWidth true if the width is required 820 * @return the signed width of the run based on the run direction; only 821 * valid if needWidth is true 822 */ 823 private float handleRun(int start, int measureLimit, 824 int limit, boolean runIsRtl, Canvas c, float x, int top, int y, 825 int bottom, FontMetricsInt fmi, boolean needWidth) { 826 827 // Case of an empty line, make sure we update fmi according to mPaint 828 if (start == measureLimit) { 829 TextPaint wp = mWorkPaint; 830 wp.set(mPaint); 831 if (fmi != null) { 832 expandMetricsFromPaint(fmi, wp); 833 } 834 return 0f; 835 } 836 837 // Shaping needs to take into account context up to metric boundaries, 838 // but rendering needs to take into account character style boundaries. 839 // So we iterate through metric runs to get metric bounds, 840 // then within each metric run iterate through character style runs 841 // for the run bounds. 842 float ox = x; 843 for (int i = start, inext; i < measureLimit; i = inext) { 844 TextPaint wp = mWorkPaint; 845 wp.set(mPaint); 846 847 int mlimit; 848 if (mSpanned == null) { 849 inext = limit; 850 mlimit = measureLimit; 851 } else { 852 inext = mSpanned.nextSpanTransition(mStart + i, mStart + limit, 853 MetricAffectingSpan.class) - mStart; 854 855 mlimit = inext < measureLimit ? inext : measureLimit; 856 MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + i, 857 mStart + mlimit, MetricAffectingSpan.class); 858 spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class); 859 860 if (spans.length > 0) { 861 ReplacementSpan replacement = null; 862 for (int j = 0; j < spans.length; j++) { 863 MetricAffectingSpan span = spans[j]; 864 if (span instanceof ReplacementSpan) { 865 replacement = (ReplacementSpan)span; 866 } else { 867 // We might have a replacement that uses the draw 868 // state, otherwise measure state would suffice. 869 span.updateDrawState(wp); 870 } 871 } 872 873 if (replacement != null) { 874 x += handleReplacement(replacement, wp, i, 875 mlimit, runIsRtl, c, x, top, y, bottom, fmi, 876 needWidth || mlimit < measureLimit); 877 continue; 878 } 879 } 880 } 881 882 if (mSpanned == null || c == null) { 883 x += handleText(wp, i, mlimit, i, inext, runIsRtl, c, x, top, 884 y, bottom, fmi, needWidth || mlimit < measureLimit); 885 } else { 886 for (int j = i, jnext; j < mlimit; j = jnext) { 887 jnext = mSpanned.nextSpanTransition(mStart + j, 888 mStart + mlimit, CharacterStyle.class) - mStart; 889 890 CharacterStyle[] spans = mSpanned.getSpans(mStart + j, 891 mStart + jnext, CharacterStyle.class); 892 spans = TextUtils.removeEmptySpans(spans, mSpanned, CharacterStyle.class); 893 894 wp.set(mPaint); 895 for (int k = 0; k < spans.length; k++) { 896 CharacterStyle span = spans[k]; 897 span.updateDrawState(wp); 898 } 899 900 x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x, 901 top, y, bottom, fmi, needWidth || jnext < measureLimit); 902 } 903 } 904 } 905 906 return x - ox; 907 } 908 909 /** 910 * Render a text run with the set-up paint. 911 * 912 * @param c the canvas 913 * @param wp the paint used to render the text 914 * @param start the start of the run 915 * @param end the end of the run 916 * @param contextStart the start of context for the run 917 * @param contextEnd the end of the context for the run 918 * @param runIsRtl true if the run is right-to-left 919 * @param x the x position of the left edge of the run 920 * @param y the baseline of the run 921 */ 922 private void drawTextRun(Canvas c, TextPaint wp, int start, int end, 923 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) { 924 925 int flags = runIsRtl ? Canvas.DIRECTION_RTL : Canvas.DIRECTION_LTR; 926 if (mCharsValid) { 927 int count = end - start; 928 int contextCount = contextEnd - contextStart; 929 c.drawTextRun(mChars, start, count, contextStart, contextCount, 930 x, y, flags, wp); 931 } else { 932 int delta = mStart; 933 c.drawTextRun(mText, delta + start, delta + end, 934 delta + contextStart, delta + contextEnd, x, y, flags, wp); 935 } 936 } 937 938 /** 939 * Returns the ascent of the text at start. This is used for scaling 940 * emoji. 941 * 942 * @param pos the line-relative position 943 * @return the ascent of the text at start 944 */ 945 float ascent(int pos) { 946 if (mSpanned == null) { 947 return mPaint.ascent(); 948 } 949 950 pos += mStart; 951 MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1, 952 MetricAffectingSpan.class); 953 if (spans.length == 0) { 954 return mPaint.ascent(); 955 } 956 957 TextPaint wp = mWorkPaint; 958 wp.set(mPaint); 959 for (MetricAffectingSpan span : spans) { 960 span.updateMeasureState(wp); 961 } 962 return wp.ascent(); 963 } 964 965 /** 966 * Returns the next tab position. 967 * 968 * @param h the (unsigned) offset from the leading margin 969 * @return the (unsigned) tab position after this offset 970 */ 971 float nextTab(float h) { 972 if (mTabs != null) { 973 return mTabs.nextTab(h); 974 } 975 return TabStops.nextDefaultStop(h, TAB_INCREMENT); 976 } 977 978 private static final int TAB_INCREMENT = 20; 979} 980