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