TextLine.java revision 52edaa9cfb612bd20b0f718dc95e576f55d9367e
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 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom, 685 previousLeading); 686 } 687 688 static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent, 689 int previousDescent, int previousBottom, int previousLeading) { 690 fmi.top = Math.min(fmi.top, previousTop); 691 fmi.ascent = Math.min(fmi.ascent, previousAscent); 692 fmi.descent = Math.max(fmi.descent, previousDescent); 693 fmi.bottom = Math.max(fmi.bottom, previousBottom); 694 fmi.leading = Math.max(fmi.leading, previousLeading); 695 } 696 697 /** 698 * Utility function for measuring and rendering text. The text must 699 * not include a tab or emoji. 700 * 701 * @param wp the working paint 702 * @param start the start of the text 703 * @param end the end of the text 704 * @param runIsRtl true if the run is right-to-left 705 * @param c the canvas, can be null if rendering is not needed 706 * @param x the edge of the run closest to the leading margin 707 * @param top the top of the line 708 * @param y the baseline 709 * @param bottom the bottom of the line 710 * @param fmi receives metrics information, can be null 711 * @param needWidth true if the width of the run is needed 712 * @return the signed width of the run based on the run direction; only 713 * valid if needWidth is true 714 */ 715 private float handleText(TextPaint wp, int start, int end, 716 int contextStart, int contextEnd, boolean runIsRtl, 717 Canvas c, float x, int top, int y, int bottom, 718 FontMetricsInt fmi, boolean needWidth) { 719 720 // Get metrics first (even for empty strings or "0" width runs) 721 if (fmi != null) { 722 expandMetricsFromPaint(fmi, wp); 723 } 724 725 int runLen = end - start; 726 // No need to do anything if the run width is "0" 727 if (runLen == 0) { 728 return 0f; 729 } 730 731 float ret = 0; 732 733 int contextLen = contextEnd - contextStart; 734 if (needWidth || (c != null && (wp.bgColor != 0 || wp.underlineColor != 0 || runIsRtl))) { 735 int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR; 736 if (mCharsValid) { 737 ret = wp.getTextRunAdvances(mChars, start, runLen, 738 contextStart, contextLen, flags, null, 0); 739 } else { 740 int delta = mStart; 741 ret = wp.getTextRunAdvances(mText, delta + start, 742 delta + end, delta + contextStart, delta + contextEnd, 743 flags, null, 0); 744 } 745 } 746 747 if (c != null) { 748 if (runIsRtl) { 749 x -= ret; 750 } 751 752 if (wp.bgColor != 0) { 753 int previousColor = wp.getColor(); 754 Paint.Style previousStyle = wp.getStyle(); 755 756 wp.setColor(wp.bgColor); 757 wp.setStyle(Paint.Style.FILL); 758 c.drawRect(x, top, x + ret, bottom, wp); 759 760 wp.setStyle(previousStyle); 761 wp.setColor(previousColor); 762 } 763 764 if (wp.underlineColor != 0) { 765 // kStdUnderline_Offset = 1/9, defined in SkTextFormatParams.h 766 float underlineTop = y + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize(); 767 768 int previousColor = wp.getColor(); 769 Paint.Style previousStyle = wp.getStyle(); 770 boolean previousAntiAlias = wp.isAntiAlias(); 771 772 wp.setStyle(Paint.Style.FILL); 773 wp.setAntiAlias(true); 774 775 wp.setColor(wp.underlineColor); 776 c.drawRect(x, underlineTop, x + ret, underlineTop + wp.underlineThickness, wp); 777 778 wp.setStyle(previousStyle); 779 wp.setColor(previousColor); 780 wp.setAntiAlias(previousAntiAlias); 781 } 782 783 drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl, 784 x, y + wp.baselineShift); 785 } 786 787 return runIsRtl ? -ret : ret; 788 } 789 790 /** 791 * Utility function for measuring and rendering a replacement. 792 * 793 * 794 * @param replacement the replacement 795 * @param wp the work paint 796 * @param start the start of the run 797 * @param limit the limit of the run 798 * @param runIsRtl true if the run is right-to-left 799 * @param c the canvas, can be null if not rendering 800 * @param x the edge of the replacement closest to the leading margin 801 * @param top the top of the line 802 * @param y the baseline 803 * @param bottom the bottom of the line 804 * @param fmi receives metrics information, can be null 805 * @param needWidth true if the width of the replacement is needed 806 * @return the signed width of the run based on the run direction; only 807 * valid if needWidth is true 808 */ 809 private float handleReplacement(ReplacementSpan replacement, TextPaint wp, 810 int start, int limit, boolean runIsRtl, Canvas c, 811 float x, int top, int y, int bottom, FontMetricsInt fmi, 812 boolean needWidth) { 813 814 float ret = 0; 815 816 int textStart = mStart + start; 817 int textLimit = mStart + limit; 818 819 if (needWidth || (c != null && runIsRtl)) { 820 int previousTop = 0; 821 int previousAscent = 0; 822 int previousDescent = 0; 823 int previousBottom = 0; 824 int previousLeading = 0; 825 826 boolean needUpdateMetrics = (fmi != null); 827 828 if (needUpdateMetrics) { 829 previousTop = fmi.top; 830 previousAscent = fmi.ascent; 831 previousDescent = fmi.descent; 832 previousBottom = fmi.bottom; 833 previousLeading = fmi.leading; 834 } 835 836 ret = replacement.getSize(wp, mText, textStart, textLimit, fmi); 837 838 if (needUpdateMetrics) { 839 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom, 840 previousLeading); 841 } 842 } 843 844 if (c != null) { 845 if (runIsRtl) { 846 x -= ret; 847 } 848 replacement.draw(c, mText, textStart, textLimit, 849 x, top, y, bottom, wp); 850 } 851 852 return runIsRtl ? -ret : ret; 853 } 854 855 private static class SpanSet<E> { 856 final int numberOfSpans; 857 final E[] spans; 858 final int[] spanStarts; 859 final int[] spanEnds; 860 final int[] spanFlags; 861 862 @SuppressWarnings("unchecked") 863 SpanSet(Spanned spanned, int start, int limit, Class<? extends E> type) { 864 final E[] allSpans = spanned.getSpans(start, limit, type); 865 final int length = allSpans.length; 866 // These arrays may end up being too large because of empty spans 867 spans = (E[]) Array.newInstance(type, length); 868 spanStarts = new int[length]; 869 spanEnds = new int[length]; 870 spanFlags = new int[length]; 871 872 int count = 0; 873 for (int i = 0; i < length; i++) { 874 final E span = allSpans[i]; 875 876 final int spanStart = spanned.getSpanStart(span); 877 final int spanEnd = spanned.getSpanEnd(span); 878 if (spanStart == spanEnd) continue; 879 880 final int spanFlag = spanned.getSpanFlags(span); 881 final int priority = spanFlag & Spanned.SPAN_PRIORITY; 882 if (priority != 0 && count != 0) { 883 int j; 884 885 for (j = 0; j < count; j++) { 886 final int otherPriority = spanFlags[j] & Spanned.SPAN_PRIORITY; 887 if (priority > otherPriority) break; 888 } 889 890 System.arraycopy(spans, j, spans, j + 1, count - j); 891 System.arraycopy(spanStarts, j, spanStarts, j + 1, count - j); 892 System.arraycopy(spanEnds, j, spanEnds, j + 1, count - j); 893 System.arraycopy(spanFlags, j, spanFlags, j + 1, count - j); 894 895 spans[j] = span; 896 spanStarts[j] = spanStart; 897 spanEnds[j] = spanEnd; 898 spanFlags[j] = spanFlag; 899 } else { 900 spans[i] = span; 901 spanStarts[i] = spanStart; 902 spanEnds[i] = spanEnd; 903 spanFlags[i] = spanFlag; 904 } 905 906 count++; 907 } 908 numberOfSpans = count; 909 } 910 911 int getNextTransition(int start, int limit) { 912 for (int i = 0; i < numberOfSpans; i++) { 913 final int spanStart = spanStarts[i]; 914 final int spanEnd = spanEnds[i]; 915 if (spanStart > start && spanStart < limit) limit = spanStart; 916 if (spanEnd > start && spanEnd < limit) limit = spanEnd; 917 } 918 return limit; 919 } 920 } 921 922 /** 923 * Utility function for handling a unidirectional run. The run must not 924 * contain tabs or emoji but can contain styles. 925 * 926 * 927 * @param start the line-relative start of the run 928 * @param measureLimit the offset to measure to, between start and limit inclusive 929 * @param limit the limit of the run 930 * @param runIsRtl true if the run is right-to-left 931 * @param c the canvas, can be null 932 * @param x the end of the run closest to the leading margin 933 * @param top the top of the line 934 * @param y the baseline 935 * @param bottom the bottom of the line 936 * @param fmi receives metrics information, can be null 937 * @param needWidth true if the width is required 938 * @return the signed width of the run based on the run direction; only 939 * valid if needWidth is true 940 */ 941 private float handleRun(int start, int measureLimit, 942 int limit, boolean runIsRtl, Canvas c, float x, int top, int y, 943 int bottom, FontMetricsInt fmi, boolean needWidth) { 944 945 // Case of an empty line, make sure we update fmi according to mPaint 946 if (start == measureLimit) { 947 TextPaint wp = mWorkPaint; 948 wp.set(mPaint); 949 if (fmi != null) { 950 expandMetricsFromPaint(fmi, wp); 951 } 952 return 0f; 953 } 954 955 if (mSpanned == null) { 956 TextPaint wp = mWorkPaint; 957 wp.set(mPaint); 958 final int mlimit = measureLimit; 959 return handleText(wp, start, mlimit, start, limit, runIsRtl, c, x, top, 960 y, bottom, fmi, needWidth || mlimit < measureLimit); 961 } 962 963 final SpanSet<MetricAffectingSpan> metricAffectingSpans = new SpanSet<MetricAffectingSpan>( 964 mSpanned, mStart + start, mStart + limit, MetricAffectingSpan.class); 965 final SpanSet<CharacterStyle> characterStyleSpans = new SpanSet<CharacterStyle>( 966 mSpanned, mStart + start, mStart + limit, CharacterStyle.class); 967 968 // Shaping needs to take into account context up to metric boundaries, 969 // but rendering needs to take into account character style boundaries. 970 // So we iterate through metric runs to get metric bounds, 971 // then within each metric run iterate through character style runs 972 // for the run bounds. 973 final float originalX = x; 974 for (int i = start, inext; i < measureLimit; i = inext) { 975 TextPaint wp = mWorkPaint; 976 wp.set(mPaint); 977 978 inext = metricAffectingSpans.getNextTransition(mStart + i, mStart + limit) - mStart; 979 int mlimit = Math.min(inext, measureLimit); 980 981 ReplacementSpan replacement = null; 982 983 for (int j = 0; j < metricAffectingSpans.numberOfSpans; j++) { 984 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT 985 // empty by construction. This special case in getSpans() explains the >= & <= tests 986 if ((metricAffectingSpans.spanStarts[j] >= mStart + mlimit) || 987 (metricAffectingSpans.spanEnds[j] <= mStart + i)) continue; 988 MetricAffectingSpan span = metricAffectingSpans.spans[j]; 989 if (span instanceof ReplacementSpan) { 990 replacement = (ReplacementSpan)span; 991 } else { 992 // We might have a replacement that uses the draw 993 // state, otherwise measure state would suffice. 994 span.updateDrawState(wp); 995 } 996 } 997 998 if (replacement != null) { 999 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y, 1000 bottom, fmi, needWidth || mlimit < measureLimit); 1001 continue; 1002 } 1003 1004 if (c == null) { 1005 x += handleText(wp, i, mlimit, i, inext, runIsRtl, c, x, top, 1006 y, bottom, fmi, needWidth || mlimit < measureLimit); 1007 } else { 1008 for (int j = i, jnext; j < mlimit; j = jnext) { 1009 jnext = characterStyleSpans.getNextTransition(mStart + j, mStart + mlimit) - 1010 mStart; 1011 1012 wp.set(mPaint); 1013 for (int k = 0; k < characterStyleSpans.numberOfSpans; k++) { 1014 // Intentionally using >= and <= as explained above 1015 if ((characterStyleSpans.spanStarts[k] >= mStart + jnext) || 1016 (characterStyleSpans.spanEnds[k] <= mStart + j)) continue; 1017 1018 CharacterStyle span = characterStyleSpans.spans[k]; 1019 span.updateDrawState(wp); 1020 } 1021 1022 x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x, 1023 top, y, bottom, fmi, needWidth || jnext < measureLimit); 1024 } 1025 } 1026 } 1027 1028 return x - originalX; 1029 } 1030 1031 /** 1032 * Render a text run with the set-up paint. 1033 * 1034 * @param c the canvas 1035 * @param wp the paint used to render the text 1036 * @param start the start of the run 1037 * @param end the end of the run 1038 * @param contextStart the start of context for the run 1039 * @param contextEnd the end of the context for the run 1040 * @param runIsRtl true if the run is right-to-left 1041 * @param x the x position of the left edge of the run 1042 * @param y the baseline of the run 1043 */ 1044 private void drawTextRun(Canvas c, TextPaint wp, int start, int end, 1045 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) { 1046 1047 int flags = runIsRtl ? Canvas.DIRECTION_RTL : Canvas.DIRECTION_LTR; 1048 if (mCharsValid) { 1049 int count = end - start; 1050 int contextCount = contextEnd - contextStart; 1051 c.drawTextRun(mChars, start, count, contextStart, contextCount, 1052 x, y, flags, wp); 1053 } else { 1054 int delta = mStart; 1055 c.drawTextRun(mText, delta + start, delta + end, 1056 delta + contextStart, delta + contextEnd, x, y, flags, wp); 1057 } 1058 } 1059 1060 /** 1061 * Returns the ascent of the text at start. This is used for scaling 1062 * emoji. 1063 * 1064 * @param pos the line-relative position 1065 * @return the ascent of the text at start 1066 */ 1067 float ascent(int pos) { 1068 if (mSpanned == null) { 1069 return mPaint.ascent(); 1070 } 1071 1072 pos += mStart; 1073 MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1, MetricAffectingSpan.class); 1074 if (spans.length == 0) { 1075 return mPaint.ascent(); 1076 } 1077 1078 TextPaint wp = mWorkPaint; 1079 wp.set(mPaint); 1080 for (MetricAffectingSpan span : spans) { 1081 span.updateMeasureState(wp); 1082 } 1083 return wp.ascent(); 1084 } 1085 1086 /** 1087 * Returns the next tab position. 1088 * 1089 * @param h the (unsigned) offset from the leading margin 1090 * @return the (unsigned) tab position after this offset 1091 */ 1092 float nextTab(float h) { 1093 if (mTabs != null) { 1094 return mTabs.nextTab(h); 1095 } 1096 return TabStops.nextDefaultStop(h, TAB_INCREMENT); 1097 } 1098 1099 private static final int TAB_INCREMENT = 20; 1100} 1101