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 int contextLen = contextEnd - contextStart; 743 if (needWidth || (c != null && (wp.bgColor != 0 || wp.underlineColor != 0 || runIsRtl))) { 744 if (mCharsValid) { 745 ret = wp.getTextRunAdvances(mChars, start, runLen, 746 contextStart, contextLen, runIsRtl, null, 0); 747 } else { 748 int delta = mStart; 749 ret = wp.getTextRunAdvances(mText, delta + start, 750 delta + end, delta + contextStart, delta + contextEnd, 751 runIsRtl, 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 */ 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)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 /** 864 * Utility function for handling a unidirectional run. The run must not 865 * contain tabs or emoji but can contain styles. 866 * 867 * 868 * @param start the line-relative start of the run 869 * @param measureLimit the offset to measure to, between start and limit inclusive 870 * @param limit the limit of the run 871 * @param runIsRtl true if the run is right-to-left 872 * @param c the canvas, can be null 873 * @param x the end of the run closest to the leading margin 874 * @param top the top of the line 875 * @param y the baseline 876 * @param bottom the bottom of the line 877 * @param fmi receives metrics information, can be null 878 * @param needWidth true if the width is required 879 * @return the signed width of the run based on the run direction; only 880 * valid if needWidth is true 881 */ handleRun(int start, int measureLimit, int limit, boolean runIsRtl, Canvas c, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth)882 private float handleRun(int start, int measureLimit, 883 int limit, boolean runIsRtl, Canvas c, float x, int top, int y, 884 int bottom, FontMetricsInt fmi, boolean needWidth) { 885 886 // Case of an empty line, make sure we update fmi according to mPaint 887 if (start == measureLimit) { 888 TextPaint wp = mWorkPaint; 889 wp.set(mPaint); 890 if (fmi != null) { 891 expandMetricsFromPaint(fmi, wp); 892 } 893 return 0f; 894 } 895 896 if (mSpanned == null) { 897 TextPaint wp = mWorkPaint; 898 wp.set(mPaint); 899 final int mlimit = measureLimit; 900 return handleText(wp, start, mlimit, start, limit, runIsRtl, c, x, top, 901 y, bottom, fmi, needWidth || mlimit < measureLimit); 902 } 903 904 mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit); 905 mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit); 906 907 // Shaping needs to take into account context up to metric boundaries, 908 // but rendering needs to take into account character style boundaries. 909 // So we iterate through metric runs to get metric bounds, 910 // then within each metric run iterate through character style runs 911 // for the run bounds. 912 final float originalX = x; 913 for (int i = start, inext; i < measureLimit; i = inext) { 914 TextPaint wp = mWorkPaint; 915 wp.set(mPaint); 916 917 inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) - 918 mStart; 919 int mlimit = Math.min(inext, measureLimit); 920 921 ReplacementSpan replacement = null; 922 923 for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) { 924 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT 925 // empty by construction. This special case in getSpans() explains the >= & <= tests 926 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) || 927 (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue; 928 MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j]; 929 if (span instanceof ReplacementSpan) { 930 replacement = (ReplacementSpan)span; 931 } else { 932 // We might have a replacement that uses the draw 933 // state, otherwise measure state would suffice. 934 span.updateDrawState(wp); 935 } 936 } 937 938 if (replacement != null) { 939 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y, 940 bottom, fmi, needWidth || mlimit < measureLimit); 941 continue; 942 } 943 944 for (int j = i, jnext; j < mlimit; j = jnext) { 945 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + mlimit) - 946 mStart; 947 948 wp.set(mPaint); 949 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) { 950 // Intentionally using >= and <= as explained above 951 if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + jnext) || 952 (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue; 953 954 CharacterStyle span = mCharacterStyleSpanSet.spans[k]; 955 span.updateDrawState(wp); 956 } 957 958 x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x, 959 top, y, bottom, fmi, needWidth || jnext < measureLimit); 960 } 961 } 962 963 return x - originalX; 964 } 965 966 /** 967 * Render a text run with the set-up paint. 968 * 969 * @param c the canvas 970 * @param wp the paint used to render the text 971 * @param start the start of the run 972 * @param end the end of the run 973 * @param contextStart the start of context for the run 974 * @param contextEnd the end of the context for the run 975 * @param runIsRtl true if the run is right-to-left 976 * @param x the x position of the left edge of the run 977 * @param y the baseline of the run 978 */ drawTextRun(Canvas c, TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x, int y)979 private void drawTextRun(Canvas c, TextPaint wp, int start, int end, 980 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) { 981 982 if (mCharsValid) { 983 int count = end - start; 984 int contextCount = contextEnd - contextStart; 985 c.drawTextRun(mChars, start, count, contextStart, contextCount, 986 x, y, runIsRtl, wp); 987 } else { 988 int delta = mStart; 989 c.drawTextRun(mText, delta + start, delta + end, 990 delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp); 991 } 992 } 993 994 /** 995 * Returns the ascent of the text at start. This is used for scaling 996 * emoji. 997 * 998 * @param pos the line-relative position 999 * @return the ascent of the text at start 1000 */ ascent(int pos)1001 float ascent(int pos) { 1002 if (mSpanned == null) { 1003 return mPaint.ascent(); 1004 } 1005 1006 pos += mStart; 1007 MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1, MetricAffectingSpan.class); 1008 if (spans.length == 0) { 1009 return mPaint.ascent(); 1010 } 1011 1012 TextPaint wp = mWorkPaint; 1013 wp.set(mPaint); 1014 for (MetricAffectingSpan span : spans) { 1015 span.updateMeasureState(wp); 1016 } 1017 return wp.ascent(); 1018 } 1019 1020 /** 1021 * Returns the next tab position. 1022 * 1023 * @param h the (unsigned) offset from the leading margin 1024 * @return the (unsigned) tab position after this offset 1025 */ nextTab(float h)1026 float nextTab(float h) { 1027 if (mTabs != null) { 1028 return mTabs.nextTab(h); 1029 } 1030 return TabStops.nextDefaultStop(h, TAB_INCREMENT); 1031 } 1032 1033 private static final int TAB_INCREMENT = 20; 1034 } 1035