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