TextLine.java revision 1e3ac18e7ad03e02819f3e1a89d6a80a2bb7645f
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 ReplacementSpan[] spans = mSpanned.getSpans(start, limit, ReplacementSpan.class); 131 spans = TextUtils.removeEmptySpans(spans, mSpanned, ReplacementSpan.class); 132 hasReplacement = spans.length > 0; 133 } 134 135 mCharsValid = hasReplacement || hasTabs || 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, ReplacementSpan.class); 151 ReplacementSpan[] spans = mSpanned.getSpans(i, inext, ReplacementSpan.class); 152 spans = TextUtils.removeEmptySpans(spans, mSpanned, ReplacementSpan.class); 153 if (spans.length > 0) { 154 // transition into a span 155 chars[i - start] = '\ufffc'; 156 for (int j = i - start + 1, e = inext - start; j < e; ++j) { 157 chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip 158 } 159 } 160 } 161 } 162 } 163 mTabs = tabStops; 164 } 165 166 /** 167 * Renders the TextLine. 168 * 169 * @param c the canvas to render on 170 * @param x the leading margin position 171 * @param top the top of the line 172 * @param y the baseline 173 * @param bottom the bottom of the line 174 */ 175 void draw(Canvas c, float x, int top, int y, int bottom) { 176 if (!mHasTabs) { 177 if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) { 178 drawRun(c, 0, 0, mLen, false, x, top, y, bottom, false); 179 return; 180 } 181 if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) { 182 drawRun(c, 0, 0, mLen, true, x, top, y, bottom, false); 183 return; 184 } 185 } 186 187 float h = 0; 188 int[] runs = mDirections.mDirections; 189 RectF emojiRect = null; 190 191 int lastRunIndex = runs.length - 2; 192 for (int i = 0; i < runs.length; i += 2) { 193 int runStart = runs[i]; 194 int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK); 195 if (runLimit > mLen) { 196 runLimit = mLen; 197 } 198 boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0; 199 200 int segstart = runStart; 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 spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class); 633 634 if (spans.length > 0) { 635 ReplacementSpan replacement = null; 636 for (int j = 0; j < spans.length; j++) { 637 MetricAffectingSpan span = spans[j]; 638 if (span instanceof ReplacementSpan) { 639 replacement = (ReplacementSpan)span; 640 } else { 641 span.updateMeasureState(wp); 642 } 643 } 644 645 if (replacement != null) { 646 // If we have a replacement span, we're moving either to 647 // the start or end of this span. 648 return after ? spanLimit : spanStart; 649 } 650 } 651 } 652 653 int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR; 654 int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE; 655 if (mCharsValid) { 656 return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart, 657 flags, offset, cursorOpt); 658 } else { 659 return wp.getTextRunCursor(mText, mStart + spanStart, 660 mStart + spanLimit, flags, mStart + offset, cursorOpt) - mStart; 661 } 662 } 663 664 /** 665 * @param wp 666 */ 667 private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) { 668 final int previousTop = fmi.top; 669 final int previousAscent = fmi.ascent; 670 final int previousDescent = fmi.descent; 671 final int previousBottom = fmi.bottom; 672 final int previousLeading = fmi.leading; 673 674 wp.getFontMetricsInt(fmi); 675 676 fmi.top = Math.min(fmi.top, previousTop); 677 fmi.ascent = Math.min(fmi.ascent, previousAscent); 678 fmi.descent = Math.max(fmi.descent, previousDescent); 679 fmi.bottom = Math.max(fmi.bottom, previousBottom); 680 fmi.leading = Math.max(fmi.leading, previousLeading); 681 } 682 683 /** 684 * Utility function for measuring and rendering text. The text must 685 * not include a tab or emoji. 686 * 687 * @param wp the working paint 688 * @param start the start of the text 689 * @param end the end of the text 690 * @param runIsRtl true if the run is right-to-left 691 * @param c the canvas, can be null if rendering is not needed 692 * @param x the edge of the run closest to the leading margin 693 * @param top the top of the line 694 * @param y the baseline 695 * @param bottom the bottom of the line 696 * @param fmi receives metrics information, can be null 697 * @param needWidth true if the width of the run is needed 698 * @return the signed width of the run based on the run direction; only 699 * valid if needWidth is true 700 */ 701 private float handleText(TextPaint wp, int start, int end, 702 int contextStart, int contextEnd, boolean runIsRtl, 703 Canvas c, float x, int top, int y, int bottom, 704 FontMetricsInt fmi, boolean needWidth) { 705 706 float ret = 0; 707 708 int runLen = end - start; 709 int contextLen = contextEnd - contextStart; 710 if (needWidth || (c != null && (wp.bgColor != 0 || runIsRtl))) { 711 int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR; 712 if (mCharsValid) { 713 ret = wp.getTextRunAdvances(mChars, start, runLen, 714 contextStart, contextLen, flags, null, 0); 715 } else { 716 int delta = mStart; 717 ret = wp.getTextRunAdvances(mText, delta + start, 718 delta + end, delta + contextStart, delta + contextEnd, 719 flags, null, 0); 720 } 721 } 722 723 if (fmi != null) { 724 expandMetricsFromPaint(fmi, wp); 725 } 726 727 if (c != null) { 728 if (runIsRtl) { 729 x -= ret; 730 } 731 732 if (wp.bgColor != 0) { 733 int color = wp.getColor(); 734 Paint.Style s = wp.getStyle(); 735 wp.setColor(wp.bgColor); 736 wp.setStyle(Paint.Style.FILL); 737 738 c.drawRect(x, top, x + ret, bottom, wp); 739 740 wp.setStyle(s); 741 wp.setColor(color); 742 } 743 744 drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl, 745 x, y + wp.baselineShift); 746 } 747 748 return runIsRtl ? -ret : ret; 749 } 750 751 /** 752 * Utility function for measuring and rendering a replacement. 753 * 754 * @param replacement the replacement 755 * @param wp the work paint 756 * @param runIndex the run index 757 * @param start the start of the run 758 * @param limit the limit of the run 759 * @param runIsRtl true if the run is right-to-left 760 * @param c the canvas, can be null if not rendering 761 * @param x the edge of the replacement closest to the leading margin 762 * @param top the top of the line 763 * @param y the baseline 764 * @param bottom the bottom of the line 765 * @param fmi receives metrics information, can be null 766 * @param needWidth true if the width of the replacement is needed 767 * @return the signed width of the run based on the run direction; only 768 * valid if needWidth is true 769 */ 770 private float handleReplacement(ReplacementSpan replacement, TextPaint wp, 771 int runIndex, int start, int limit, boolean runIsRtl, Canvas c, 772 float x, int top, int y, int bottom, FontMetricsInt fmi, 773 boolean needWidth) { 774 775 float ret = 0; 776 777 int textStart = mStart + start; 778 int textLimit = mStart + limit; 779 780 if (needWidth || (c != null && runIsRtl)) { 781 ret = replacement.getSize(wp, mText, textStart, textLimit, fmi); 782 } 783 784 if (c != null) { 785 if (runIsRtl) { 786 x -= ret; 787 } 788 replacement.draw(c, mText, textStart, textLimit, 789 x, top, y, bottom, wp); 790 } 791 792 return runIsRtl ? -ret : ret; 793 } 794 795 /** 796 * Utility function for handling a unidirectional run. The run must not 797 * contain tabs or emoji but can contain styles. 798 * 799 * @param runIndex the run index 800 * @param start the line-relative start of the run 801 * @param measureLimit the offset to measure to, between start and limit inclusive 802 * @param limit the limit of the run 803 * @param runIsRtl true if the run is right-to-left 804 * @param c the canvas, can be null 805 * @param x the end of the run closest to the leading margin 806 * @param top the top of the line 807 * @param y the baseline 808 * @param bottom the bottom of the line 809 * @param fmi receives metrics information, can be null 810 * @param needWidth true if the width is required 811 * @return the signed width of the run based on the run direction; only 812 * valid if needWidth is true 813 */ 814 private float handleRun(int runIndex, int start, int measureLimit, 815 int limit, boolean runIsRtl, Canvas c, float x, int top, int y, 816 int bottom, FontMetricsInt fmi, boolean needWidth) { 817 818 // Shaping needs to take into account context up to metric boundaries, 819 // but rendering needs to take into account character style boundaries. 820 // So we iterate through metric runs to get metric bounds, 821 // then within each metric run iterate through character style runs 822 // for the run bounds. 823 float ox = x; 824 for (int i = start, inext; i < measureLimit; i = inext) { 825 TextPaint wp = mWorkPaint; 826 wp.set(mPaint); 827 828 int mlimit; 829 if (mSpanned == null) { 830 inext = limit; 831 mlimit = measureLimit; 832 } else { 833 inext = mSpanned.nextSpanTransition(mStart + i, mStart + limit, 834 MetricAffectingSpan.class) - mStart; 835 836 mlimit = inext < measureLimit ? inext : measureLimit; 837 MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + i, 838 mStart + mlimit, MetricAffectingSpan.class); 839 spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class); 840 841 if (spans.length > 0) { 842 ReplacementSpan replacement = null; 843 for (int j = 0; j < spans.length; j++) { 844 MetricAffectingSpan span = spans[j]; 845 if (span instanceof ReplacementSpan) { 846 replacement = (ReplacementSpan)span; 847 } else { 848 // We might have a replacement that uses the draw 849 // state, otherwise measure state would suffice. 850 span.updateDrawState(wp); 851 } 852 } 853 854 if (replacement != null) { 855 x += handleReplacement(replacement, wp, runIndex, i, 856 mlimit, runIsRtl, c, x, top, y, bottom, fmi, 857 needWidth || mlimit < measureLimit); 858 continue; 859 } 860 } 861 } 862 863 if (mSpanned == null || c == null) { 864 x += handleText(wp, i, mlimit, i, inext, runIsRtl, c, x, top, 865 y, bottom, fmi, needWidth || mlimit < measureLimit); 866 } else { 867 for (int j = i, jnext; j < mlimit; j = jnext) { 868 jnext = mSpanned.nextSpanTransition(mStart + j, 869 mStart + mlimit, CharacterStyle.class) - mStart; 870 871 CharacterStyle[] spans = mSpanned.getSpans(mStart + j, 872 mStart + jnext, CharacterStyle.class); 873 spans = TextUtils.removeEmptySpans(spans, mSpanned, CharacterStyle.class); 874 875 wp.set(mPaint); 876 for (int k = 0; k < spans.length; k++) { 877 CharacterStyle span = spans[k]; 878 span.updateDrawState(wp); 879 } 880 881 x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x, 882 top, y, bottom, fmi, needWidth || jnext < measureLimit); 883 } 884 } 885 } 886 887 return x - ox; 888 } 889 890 /** 891 * Render a text run with the set-up paint. 892 * 893 * @param c the canvas 894 * @param wp the paint used to render the text 895 * @param start the start of the run 896 * @param end the end of the run 897 * @param contextStart the start of context for the run 898 * @param contextEnd the end of the context for the run 899 * @param runIsRtl true if the run is right-to-left 900 * @param x the x position of the left edge of the run 901 * @param y the baseline of the run 902 */ 903 private void drawTextRun(Canvas c, TextPaint wp, int start, int end, 904 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) { 905 906 int flags = runIsRtl ? Canvas.DIRECTION_RTL : Canvas.DIRECTION_LTR; 907 if (mCharsValid) { 908 int count = end - start; 909 int contextCount = contextEnd - contextStart; 910 c.drawTextRun(mChars, start, count, contextStart, contextCount, 911 x, y, flags, wp); 912 } else { 913 int delta = mStart; 914 c.drawTextRun(mText, delta + start, delta + end, 915 delta + contextStart, delta + contextEnd, x, y, flags, wp); 916 } 917 } 918 919 /** 920 * Returns the ascent of the text at start. This is used for scaling 921 * emoji. 922 * 923 * @param pos the line-relative position 924 * @return the ascent of the text at start 925 */ 926 float ascent(int pos) { 927 if (mSpanned == null) { 928 return mPaint.ascent(); 929 } 930 931 pos += mStart; 932 MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1, 933 MetricAffectingSpan.class); 934 if (spans.length == 0) { 935 return mPaint.ascent(); 936 } 937 938 TextPaint wp = mWorkPaint; 939 wp.set(mPaint); 940 for (MetricAffectingSpan span : spans) { 941 span.updateMeasureState(wp); 942 } 943 return wp.ascent(); 944 } 945 946 /** 947 * Returns the next tab position. 948 * 949 * @param h the (unsigned) offset from the leading margin 950 * @return the (unsigned) tab position after this offset 951 */ 952 float nextTab(float h) { 953 if (mTabs != null) { 954 return mTabs.nextTab(h); 955 } 956 return TabStops.nextDefaultStop(h, TAB_INCREMENT); 957 } 958 959 private static final int TAB_INCREMENT = 20; 960} 961