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