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