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