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