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 */ obtain()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 */ recycle(TextLine tl)99 static TextLine recycle(TextLine tl) { 100 tl.mText = null; 101 tl.mPaint = null; 102 tl.mDirections = null; 103 tl.mSpanned = null; 104 tl.mTabs = null; 105 tl.mChars = null; 106 107 tl.mMetricAffectingSpanSpanSet.recycle(); 108 tl.mCharacterStyleSpanSet.recycle(); 109 tl.mReplacementSpanSpanSet.recycle(); 110 111 synchronized(sCached) { 112 for (int i = 0; i < sCached.length; ++i) { 113 if (sCached[i] == null) { 114 sCached[i] = tl; 115 break; 116 } 117 } 118 } 119 return null; 120 } 121 122 /** 123 * Initializes a TextLine and prepares it for use. 124 * 125 * @param paint the base paint for the line 126 * @param text the text, can be Styled 127 * @param start the start of the line relative to the text 128 * @param limit the limit of the line relative to the text 129 * @param dir the paragraph direction of this line 130 * @param directions the directions information of this line 131 * @param hasTabs true if the line might contain tabs or emoji 132 * @param tabStops the tabStops. Can be null. 133 */ set(TextPaint paint, CharSequence text, int start, int limit, int dir, Directions directions, boolean hasTabs, TabStops tabStops)134 void set(TextPaint paint, CharSequence text, int start, int limit, int dir, 135 Directions directions, boolean hasTabs, TabStops tabStops) { 136 mPaint = paint; 137 mText = text; 138 mStart = start; 139 mLen = limit - start; 140 mDir = dir; 141 mDirections = directions; 142 if (mDirections == null) { 143 throw new IllegalArgumentException("Directions cannot be null"); 144 } 145 mHasTabs = hasTabs; 146 mSpanned = null; 147 148 boolean hasReplacement = false; 149 if (text instanceof Spanned) { 150 mSpanned = (Spanned) text; 151 mReplacementSpanSpanSet.init(mSpanned, start, limit); 152 hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0; 153 } 154 155 mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT; 156 157 if (mCharsValid) { 158 if (mChars == null || mChars.length < mLen) { 159 mChars = ArrayUtils.newUnpaddedCharArray(mLen); 160 } 161 TextUtils.getChars(text, start, limit, mChars, 0); 162 if (hasReplacement) { 163 // Handle these all at once so we don't have to do it as we go. 164 // Replace the first character of each replacement run with the 165 // object-replacement character and the remainder with zero width 166 // non-break space aka BOM. Cursor movement code skips these 167 // zero-width characters. 168 char[] chars = mChars; 169 for (int i = start, inext; i < limit; i = inext) { 170 inext = mReplacementSpanSpanSet.getNextTransition(i, limit); 171 if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)) { 172 // transition into a span 173 chars[i - start] = '\ufffc'; 174 for (int j = i - start + 1, e = inext - start; j < e; ++j) { 175 chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip 176 } 177 } 178 } 179 } 180 } 181 mTabs = tabStops; 182 } 183 184 /** 185 * Renders the TextLine. 186 * 187 * @param c the canvas to render on 188 * @param x the leading margin position 189 * @param top the top of the line 190 * @param y the baseline 191 * @param bottom the bottom of the line 192 */ draw(Canvas c, float x, int top, int y, int bottom)193 void draw(Canvas c, float x, int top, int y, int bottom) { 194 if (!mHasTabs) { 195 if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) { 196 drawRun(c, 0, mLen, false, x, top, y, bottom, false); 197 return; 198 } 199 if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) { 200 drawRun(c, 0, mLen, true, x, top, y, bottom, false); 201 return; 202 } 203 } 204 205 float h = 0; 206 int[] runs = mDirections.mDirections; 207 RectF emojiRect = null; 208 209 int lastRunIndex = runs.length - 2; 210 for (int i = 0; i < runs.length; i += 2) { 211 int runStart = runs[i]; 212 int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK); 213 if (runLimit > mLen) { 214 runLimit = mLen; 215 } 216 boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0; 217 218 int segstart = runStart; 219 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { 220 int codept = 0; 221 Bitmap bm = null; 222 223 if (mHasTabs && j < runLimit) { 224 codept = mChars[j]; 225 if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) { 226 codept = Character.codePointAt(mChars, j); 227 if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) { 228 bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept); 229 } else if (codept > 0xffff) { 230 ++j; 231 continue; 232 } 233 } 234 } 235 236 if (j == runLimit || codept == '\t' || bm != null) { 237 h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom, 238 i != lastRunIndex || j != mLen); 239 240 if (codept == '\t') { 241 h = mDir * nextTab(h * mDir); 242 } else if (bm != null) { 243 float bmAscent = ascent(j); 244 float bitmapHeight = bm.getHeight(); 245 float scale = -bmAscent / bitmapHeight; 246 float width = bm.getWidth() * scale; 247 248 if (emojiRect == null) { 249 emojiRect = new RectF(); 250 } 251 emojiRect.set(x + h, y + bmAscent, 252 x + h + width, y); 253 c.drawBitmap(bm, null, emojiRect, mPaint); 254 h += width; 255 j++; 256 } 257 segstart = j + 1; 258 } 259 } 260 } 261 } 262 263 /** 264 * Returns metrics information for the entire line. 265 * 266 * @param fmi receives font metrics information, can be null 267 * @return the signed width of the line 268 */ metrics(FontMetricsInt fmi)269 float metrics(FontMetricsInt fmi) { 270 return measure(mLen, false, fmi); 271 } 272 273 /** 274 * Returns information about a position on the line. 275 * 276 * @param offset the line-relative character offset, between 0 and the 277 * line length, inclusive 278 * @param trailing true to measure the trailing edge of the character 279 * before offset, false to measure the leading edge of the character 280 * at offset. 281 * @param fmi receives metrics information about the requested 282 * character, can be null. 283 * @return the signed offset from the leading margin to the requested 284 * character edge. 285 */ measure(int offset, boolean trailing, FontMetricsInt fmi)286 float measure(int offset, boolean trailing, FontMetricsInt fmi) { 287 int target = trailing ? offset - 1 : offset; 288 if (target < 0) { 289 return 0; 290 } 291 292 float h = 0; 293 294 if (!mHasTabs) { 295 if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) { 296 return measureRun(0, offset, mLen, false, fmi); 297 } 298 if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) { 299 return measureRun(0, offset, mLen, true, fmi); 300 } 301 } 302 303 char[] chars = mChars; 304 int[] runs = mDirections.mDirections; 305 for (int i = 0; i < runs.length; i += 2) { 306 int runStart = runs[i]; 307 int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK); 308 if (runLimit > mLen) { 309 runLimit = mLen; 310 } 311 boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0; 312 313 int segstart = runStart; 314 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { 315 int codept = 0; 316 Bitmap bm = null; 317 318 if (mHasTabs && j < runLimit) { 319 codept = chars[j]; 320 if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) { 321 codept = Character.codePointAt(chars, j); 322 if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) { 323 bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept); 324 } else if (codept > 0xffff) { 325 ++j; 326 continue; 327 } 328 } 329 } 330 331 if (j == runLimit || codept == '\t' || bm != null) { 332 boolean inSegment = target >= segstart && target < j; 333 334 boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl; 335 if (inSegment && advance) { 336 return h += measureRun(segstart, offset, j, runIsRtl, fmi); 337 } 338 339 float w = measureRun(segstart, j, j, runIsRtl, fmi); 340 h += advance ? w : -w; 341 342 if (inSegment) { 343 return h += measureRun(segstart, offset, j, runIsRtl, null); 344 } 345 346 if (codept == '\t') { 347 if (offset == j) { 348 return h; 349 } 350 h = mDir * nextTab(h * mDir); 351 if (target == j) { 352 return h; 353 } 354 } 355 356 if (bm != null) { 357 float bmAscent = ascent(j); 358 float wid = bm.getWidth() * -bmAscent / bm.getHeight(); 359 h += mDir * wid; 360 j++; 361 } 362 363 segstart = j + 1; 364 } 365 } 366 } 367 368 return h; 369 } 370 371 /** 372 * Draws a unidirectional (but possibly multi-styled) run of text. 373 * 374 * 375 * @param c the canvas to draw on 376 * @param start the line-relative start 377 * @param limit the line-relative limit 378 * @param runIsRtl true if the run is right-to-left 379 * @param x the position of the run that is closest to the leading margin 380 * @param top the top of the line 381 * @param y the baseline 382 * @param bottom the bottom of the line 383 * @param needWidth true if the width value is required. 384 * @return the signed width of the run, based on the paragraph direction. 385 * Only valid if needWidth is true. 386 */ 387 private float drawRun(Canvas c, int start, 388 int limit, boolean runIsRtl, float x, int top, int y, int bottom, 389 boolean needWidth) { 390 391 if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { 392 float w = -measureRun(start, limit, limit, runIsRtl, null); 393 handleRun(start, limit, limit, runIsRtl, c, x + w, top, 394 y, bottom, null, false); 395 return w; 396 } 397 398 return handleRun(start, limit, limit, runIsRtl, c, x, top, 399 y, bottom, null, needWidth); 400 } 401 402 /** 403 * Measures a unidirectional (but possibly multi-styled) run of text. 404 * 405 * 406 * @param start the line-relative start of the run 407 * @param offset the offset to measure to, between start and limit inclusive 408 * @param limit the line-relative limit of the run 409 * @param runIsRtl true if the run is right-to-left 410 * @param fmi receives metrics information about the requested 411 * run, can be null. 412 * @return the signed width from the start of the run to the leading edge 413 * of the character at offset, based on the run (not paragraph) direction 414 */ 415 private float measureRun(int start, int offset, int limit, boolean runIsRtl, 416 FontMetricsInt fmi) { 417 return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true); 418 } 419 420 /** 421 * Walk the cursor through this line, skipping conjuncts and 422 * zero-width characters. 423 * 424 * <p>This function cannot properly walk the cursor off the ends of the line 425 * since it does not know about any shaping on the previous/following line 426 * that might affect the cursor position. Callers must either avoid these 427 * situations or handle the result specially. 428 * 429 * @param cursor the starting position of the cursor, between 0 and the 430 * length of the line, inclusive 431 * @param toLeft true if the caret is moving to the left. 432 * @return the new offset. If it is less than 0 or greater than the length 433 * of the line, the previous/following line should be examined to get the 434 * actual offset. 435 */ 436 int getOffsetToLeftRightOf(int cursor, boolean toLeft) { 437 // 1) The caret marks the leading edge of a character. The character 438 // logically before it might be on a different level, and the active caret 439 // position is on the character at the lower level. If that character 440 // was the previous character, the caret is on its trailing edge. 441 // 2) Take this character/edge and move it in the indicated direction. 442 // This gives you a new character and a new edge. 443 // 3) This position is between two visually adjacent characters. One of 444 // these might be at a lower level. The active position is on the 445 // character at the lower level. 446 // 4) If the active position is on the trailing edge of the character, 447 // the new caret position is the following logical character, else it 448 // is the character. 449 450 int lineStart = 0; 451 int lineEnd = mLen; 452 boolean paraIsRtl = mDir == -1; 453 int[] runs = mDirections.mDirections; 454 455 int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1; 456 boolean trailing = false; 457 458 if (cursor == lineStart) { 459 runIndex = -2; 460 } else if (cursor == lineEnd) { 461 runIndex = runs.length; 462 } else { 463 // First, get information about the run containing the character with 464 // the active caret. 465 for (runIndex = 0; runIndex < runs.length; runIndex += 2) { 466 runStart = lineStart + runs[runIndex]; 467 if (cursor >= runStart) { 468 runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK); 469 if (runLimit > lineEnd) { 470 runLimit = lineEnd; 471 } 472 if (cursor < runLimit) { 473 runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) & 474 Layout.RUN_LEVEL_MASK; 475 if (cursor == runStart) { 476 // The caret is on a run boundary, see if we should 477 // use the position on the trailing edge of the previous 478 // logical character instead. 479 int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit; 480 int pos = cursor - 1; 481 for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) { 482 prevRunStart = lineStart + runs[prevRunIndex]; 483 if (pos >= prevRunStart) { 484 prevRunLimit = prevRunStart + 485 (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK); 486 if (prevRunLimit > lineEnd) { 487 prevRunLimit = lineEnd; 488 } 489 if (pos < prevRunLimit) { 490 prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) 491 & Layout.RUN_LEVEL_MASK; 492 if (prevRunLevel < runLevel) { 493 // Start from logically previous character. 494 runIndex = prevRunIndex; 495 runLevel = prevRunLevel; 496 runStart = prevRunStart; 497 runLimit = prevRunLimit; 498 trailing = true; 499 break; 500 } 501 } 502 } 503 } 504 } 505 break; 506 } 507 } 508 } 509 510 // caret might be == lineEnd. This is generally a space or paragraph 511 // separator and has an associated run, but might be the end of 512 // text, in which case it doesn't. If that happens, we ran off the 513 // end of the run list, and runIndex == runs.length. In this case, 514 // we are at a run boundary so we skip the below test. 515 if (runIndex != runs.length) { 516 boolean runIsRtl = (runLevel & 0x1) != 0; 517 boolean advance = toLeft == runIsRtl; 518 if (cursor != (advance ? runLimit : runStart) || advance != trailing) { 519 // Moving within or into the run, so we can move logically. 520 newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit, 521 runIsRtl, cursor, advance); 522 // If the new position is internal to the run, we're at the strong 523 // position already so we're finished. 524 if (newCaret != (advance ? runLimit : runStart)) { 525 return newCaret; 526 } 527 } 528 } 529 } 530 531 // If newCaret is -1, we're starting at a run boundary and crossing 532 // into another run. Otherwise we've arrived at a run boundary, and 533 // need to figure out which character to attach to. Note we might 534 // need to run this twice, if we cross a run boundary and end up at 535 // another run boundary. 536 while (true) { 537 boolean advance = toLeft == paraIsRtl; 538 int otherRunIndex = runIndex + (advance ? 2 : -2); 539 if (otherRunIndex >= 0 && otherRunIndex < runs.length) { 540 int otherRunStart = lineStart + runs[otherRunIndex]; 541 int otherRunLimit = otherRunStart + 542 (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK); 543 if (otherRunLimit > lineEnd) { 544 otherRunLimit = lineEnd; 545 } 546 int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) & 547 Layout.RUN_LEVEL_MASK; 548 boolean otherRunIsRtl = (otherRunLevel & 1) != 0; 549 550 advance = toLeft == otherRunIsRtl; 551 if (newCaret == -1) { 552 newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart, 553 otherRunLimit, otherRunIsRtl, 554 advance ? otherRunStart : otherRunLimit, advance); 555 if (newCaret == (advance ? otherRunLimit : otherRunStart)) { 556 // Crossed and ended up at a new boundary, 557 // repeat a second and final time. 558 runIndex = otherRunIndex; 559 runLevel = otherRunLevel; 560 continue; 561 } 562 break; 563 } 564 565 // The new caret is at a boundary. 566 if (otherRunLevel < runLevel) { 567 // The strong character is in the other run. 568 newCaret = advance ? otherRunStart : otherRunLimit; 569 } 570 break; 571 } 572 573 if (newCaret == -1) { 574 // We're walking off the end of the line. The paragraph 575 // level is always equal to or lower than any internal level, so 576 // the boundaries get the strong caret. 577 newCaret = advance ? mLen + 1 : -1; 578 break; 579 } 580 581 // Else we've arrived at the end of the line. That's a strong position. 582 // We might have arrived here by crossing over a run with no internal 583 // breaks and dropping out of the above loop before advancing one final 584 // time, so reset the caret. 585 // Note, we use '<=' below to handle a situation where the only run 586 // on the line is a counter-directional run. If we're not advancing, 587 // we can end up at the 'lineEnd' position but the caret we want is at 588 // the lineStart. 589 if (newCaret <= lineEnd) { 590 newCaret = advance ? lineEnd : lineStart; 591 } 592 break; 593 } 594 595 return newCaret; 596 } 597 598 /** 599 * Returns the next valid offset within this directional run, skipping 600 * conjuncts and zero-width characters. This should not be called to walk 601 * off the end of the line, since the returned values might not be valid 602 * on neighboring lines. If the returned offset is less than zero or 603 * greater than the line length, the offset should be recomputed on the 604 * preceding or following line, respectively. 605 * 606 * @param runIndex the run index 607 * @param runStart the start of the run 608 * @param runLimit the limit of the run 609 * @param runIsRtl true if the run is right-to-left 610 * @param offset the offset 611 * @param after true if the new offset should logically follow the provided 612 * offset 613 * @return the new offset 614 */ getOffsetBeforeAfter(int runIndex, int runStart, int runLimit, boolean runIsRtl, int offset, boolean after)615 private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit, 616 boolean runIsRtl, int offset, boolean after) { 617 618 if (runIndex < 0 || offset == (after ? mLen : 0)) { 619 // Walking off end of line. Since we don't know 620 // what cursor positions are available on other lines, we can't 621 // return accurate values. These are a guess. 622 if (after) { 623 return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart; 624 } 625 return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart; 626 } 627 628 TextPaint wp = mWorkPaint; 629 wp.set(mPaint); 630 631 int spanStart = runStart; 632 int spanLimit; 633 if (mSpanned == null) { 634 spanLimit = runLimit; 635 } else { 636 int target = after ? offset + 1 : offset; 637 int limit = mStart + runLimit; 638 while (true) { 639 spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit, 640 MetricAffectingSpan.class) - mStart; 641 if (spanLimit >= target) { 642 break; 643 } 644 spanStart = spanLimit; 645 } 646 647 MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart, 648 mStart + spanLimit, MetricAffectingSpan.class); 649 spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class); 650 651 if (spans.length > 0) { 652 ReplacementSpan replacement = null; 653 for (int j = 0; j < spans.length; j++) { 654 MetricAffectingSpan span = spans[j]; 655 if (span instanceof ReplacementSpan) { 656 replacement = (ReplacementSpan)span; 657 } else { 658 span.updateMeasureState(wp); 659 } 660 } 661 662 if (replacement != null) { 663 // If we have a replacement span, we're moving either to 664 // the start or end of this span. 665 return after ? spanLimit : spanStart; 666 } 667 } 668 } 669 670 int dir = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR; 671 int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE; 672 if (mCharsValid) { 673 return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart, 674 dir, offset, cursorOpt); 675 } else { 676 return wp.getTextRunCursor(mText, mStart + spanStart, 677 mStart + spanLimit, dir, mStart + offset, cursorOpt) - mStart; 678 } 679 } 680 681 /** 682 * @param wp 683 */ expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp)684 private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) { 685 final int previousTop = fmi.top; 686 final int previousAscent = fmi.ascent; 687 final int previousDescent = fmi.descent; 688 final int previousBottom = fmi.bottom; 689 final int previousLeading = fmi.leading; 690 691 wp.getFontMetricsInt(fmi); 692 693 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom, 694 previousLeading); 695 } 696 updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent, int previousDescent, int previousBottom, int previousLeading)697 static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent, 698 int previousDescent, int previousBottom, int previousLeading) { 699 fmi.top = Math.min(fmi.top, previousTop); 700 fmi.ascent = Math.min(fmi.ascent, previousAscent); 701 fmi.descent = Math.max(fmi.descent, previousDescent); 702 fmi.bottom = Math.max(fmi.bottom, previousBottom); 703 fmi.leading = Math.max(fmi.leading, previousLeading); 704 } 705 706 /** 707 * Utility function for measuring and rendering text. The text must 708 * not include a tab or emoji. 709 * 710 * @param wp the working paint 711 * @param start the start of the text 712 * @param end the end of the text 713 * @param runIsRtl true if the run is right-to-left 714 * @param c the canvas, can be null if rendering is not needed 715 * @param x the edge of the run closest to the leading margin 716 * @param top the top of the line 717 * @param y the baseline 718 * @param bottom the bottom of the line 719 * @param fmi receives metrics information, can be null 720 * @param needWidth true if the width of the run is needed 721 * @return the signed width of the run based on the run direction; only 722 * valid if needWidth is true 723 */ handleText(TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, Canvas c, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth)724 private float handleText(TextPaint wp, int start, int end, 725 int contextStart, int contextEnd, boolean runIsRtl, 726 Canvas c, float x, int top, int y, int bottom, 727 FontMetricsInt fmi, boolean needWidth) { 728 729 // Get metrics first (even for empty strings or "0" width runs) 730 if (fmi != null) { 731 expandMetricsFromPaint(fmi, wp); 732 } 733 734 int runLen = end - start; 735 // No need to do anything if the run width is "0" 736 if (runLen == 0) { 737 return 0f; 738 } 739 740 float ret = 0; 741 742 if (needWidth || (c != null && (wp.bgColor != 0 || wp.underlineColor != 0 || runIsRtl))) { 743 if (mCharsValid) { 744 ret = wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, 745 runIsRtl, end); 746 } else { 747 int delta = mStart; 748 ret = wp.getRunAdvance(mText, delta + start, delta + end, 749 delta + contextStart, delta + contextEnd, runIsRtl, delta + end); 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 */ handleReplacement(ReplacementSpan replacement, TextPaint wp, int start, int limit, boolean runIsRtl, Canvas c, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth)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 */ handleRun(int start, int measureLimit, int limit, boolean runIsRtl, Canvas c, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth)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 // Only draw hyphen on last run in line 957 if (jnext < mLen) { 958 wp.setHyphenEdit(0); 959 } 960 x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x, 961 top, y, bottom, fmi, needWidth || jnext < measureLimit); 962 } 963 } 964 965 return x - originalX; 966 } 967 968 /** 969 * Render a text run with the set-up paint. 970 * 971 * @param c the canvas 972 * @param wp the paint used to render the text 973 * @param start the start of the run 974 * @param end the end of the run 975 * @param contextStart the start of context for the run 976 * @param contextEnd the end of the context for the run 977 * @param runIsRtl true if the run is right-to-left 978 * @param x the x position of the left edge of the run 979 * @param y the baseline of the run 980 */ drawTextRun(Canvas c, TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x, int y)981 private void drawTextRun(Canvas c, TextPaint wp, int start, int end, 982 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) { 983 984 if (mCharsValid) { 985 int count = end - start; 986 int contextCount = contextEnd - contextStart; 987 c.drawTextRun(mChars, start, count, contextStart, contextCount, 988 x, y, runIsRtl, wp); 989 } else { 990 int delta = mStart; 991 c.drawTextRun(mText, delta + start, delta + end, 992 delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp); 993 } 994 } 995 996 /** 997 * Returns the ascent of the text at start. This is used for scaling 998 * emoji. 999 * 1000 * @param pos the line-relative position 1001 * @return the ascent of the text at start 1002 */ ascent(int pos)1003 float ascent(int pos) { 1004 if (mSpanned == null) { 1005 return mPaint.ascent(); 1006 } 1007 1008 pos += mStart; 1009 MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1, MetricAffectingSpan.class); 1010 if (spans.length == 0) { 1011 return mPaint.ascent(); 1012 } 1013 1014 TextPaint wp = mWorkPaint; 1015 wp.set(mPaint); 1016 for (MetricAffectingSpan span : spans) { 1017 span.updateMeasureState(wp); 1018 } 1019 return wp.ascent(); 1020 } 1021 1022 /** 1023 * Returns the next tab position. 1024 * 1025 * @param h the (unsigned) offset from the leading margin 1026 * @return the (unsigned) tab position after this offset 1027 */ nextTab(float h)1028 float nextTab(float h) { 1029 if (mTabs != null) { 1030 return mTabs.nextTab(h); 1031 } 1032 return TabStops.nextDefaultStop(h, TAB_INCREMENT); 1033 } 1034 1035 private static final int TAB_INCREMENT = 20; 1036 } 1037