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