1/* 2 * Copyright (C) 2006 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 android.graphics.Paint; 20import android.text.style.UpdateLayout; 21import android.text.style.WrapTogetherSpan; 22 23import com.android.internal.util.ArrayUtils; 24import com.android.internal.util.GrowingArrayUtils; 25 26import java.lang.ref.WeakReference; 27 28/** 29 * DynamicLayout is a text layout that updates itself as the text is edited. 30 * <p>This is used by widgets to control text layout. You should not need 31 * to use this class directly unless you are implementing your own widget 32 * or custom display object, or need to call 33 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint) 34 * Canvas.drawText()} directly.</p> 35 */ 36public class DynamicLayout extends Layout 37{ 38 private static final int PRIORITY = 128; 39 private static final int BLOCK_MINIMUM_CHARACTER_LENGTH = 400; 40 41 /** 42 * Make a layout for the specified text that will be updated as 43 * the text is changed. 44 */ 45 public DynamicLayout(CharSequence base, 46 TextPaint paint, 47 int width, Alignment align, 48 float spacingmult, float spacingadd, 49 boolean includepad) { 50 this(base, base, paint, width, align, spacingmult, spacingadd, 51 includepad); 52 } 53 54 /** 55 * Make a layout for the transformed text (password transformation 56 * being the primary example of a transformation) 57 * that will be updated as the base text is changed. 58 */ 59 public DynamicLayout(CharSequence base, CharSequence display, 60 TextPaint paint, 61 int width, Alignment align, 62 float spacingmult, float spacingadd, 63 boolean includepad) { 64 this(base, display, paint, width, align, spacingmult, spacingadd, 65 includepad, null, 0); 66 } 67 68 /** 69 * Make a layout for the transformed text (password transformation 70 * being the primary example of a transformation) 71 * that will be updated as the base text is changed. 72 * If ellipsize is non-null, the Layout will ellipsize the text 73 * down to ellipsizedWidth. 74 */ 75 public DynamicLayout(CharSequence base, CharSequence display, 76 TextPaint paint, 77 int width, Alignment align, 78 float spacingmult, float spacingadd, 79 boolean includepad, 80 TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { 81 this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR, 82 spacingmult, spacingadd, includepad, 83 StaticLayout.BREAK_STRATEGY_SIMPLE, StaticLayout.HYPHENATION_FREQUENCY_NONE, 84 ellipsize, ellipsizedWidth); 85 } 86 87 /** 88 * Make a layout for the transformed text (password transformation 89 * being the primary example of a transformation) 90 * that will be updated as the base text is changed. 91 * If ellipsize is non-null, the Layout will ellipsize the text 92 * down to ellipsizedWidth. 93 * * 94 * *@hide 95 */ 96 public DynamicLayout(CharSequence base, CharSequence display, 97 TextPaint paint, 98 int width, Alignment align, TextDirectionHeuristic textDir, 99 float spacingmult, float spacingadd, 100 boolean includepad, int breakStrategy, int hyphenationFrequency, 101 TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { 102 super((ellipsize == null) 103 ? display 104 : (display instanceof Spanned) 105 ? new SpannedEllipsizer(display) 106 : new Ellipsizer(display), 107 paint, width, align, textDir, spacingmult, spacingadd); 108 109 mBase = base; 110 mDisplay = display; 111 112 if (ellipsize != null) { 113 mInts = new PackedIntVector(COLUMNS_ELLIPSIZE); 114 mEllipsizedWidth = ellipsizedWidth; 115 mEllipsizeAt = ellipsize; 116 } else { 117 mInts = new PackedIntVector(COLUMNS_NORMAL); 118 mEllipsizedWidth = width; 119 mEllipsizeAt = null; 120 } 121 122 mObjects = new PackedObjectVector<Directions>(1); 123 124 mIncludePad = includepad; 125 mBreakStrategy = breakStrategy; 126 mHyphenationFrequency = hyphenationFrequency; 127 128 /* 129 * This is annoying, but we can't refer to the layout until 130 * superclass construction is finished, and the superclass 131 * constructor wants the reference to the display text. 132 * 133 * This will break if the superclass constructor ever actually 134 * cares about the content instead of just holding the reference. 135 */ 136 if (ellipsize != null) { 137 Ellipsizer e = (Ellipsizer) getText(); 138 139 e.mLayout = this; 140 e.mWidth = ellipsizedWidth; 141 e.mMethod = ellipsize; 142 mEllipsize = true; 143 } 144 145 // Initial state is a single line with 0 characters (0 to 0), 146 // with top at 0 and bottom at whatever is natural, and 147 // undefined ellipsis. 148 149 int[] start; 150 151 if (ellipsize != null) { 152 start = new int[COLUMNS_ELLIPSIZE]; 153 start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED; 154 } else { 155 start = new int[COLUMNS_NORMAL]; 156 } 157 158 Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT }; 159 160 Paint.FontMetricsInt fm = paint.getFontMetricsInt(); 161 int asc = fm.ascent; 162 int desc = fm.descent; 163 164 start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT; 165 start[TOP] = 0; 166 start[DESCENT] = desc; 167 mInts.insertAt(0, start); 168 169 start[TOP] = desc - asc; 170 mInts.insertAt(1, start); 171 172 mObjects.insertAt(0, dirs); 173 174 // Update from 0 characters to whatever the real text is 175 reflow(base, 0, 0, base.length()); 176 177 if (base instanceof Spannable) { 178 if (mWatcher == null) 179 mWatcher = new ChangeWatcher(this); 180 181 // Strip out any watchers for other DynamicLayouts. 182 Spannable sp = (Spannable) base; 183 ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class); 184 for (int i = 0; i < spans.length; i++) 185 sp.removeSpan(spans[i]); 186 187 sp.setSpan(mWatcher, 0, base.length(), 188 Spannable.SPAN_INCLUSIVE_INCLUSIVE | 189 (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT)); 190 } 191 } 192 193 private void reflow(CharSequence s, int where, int before, int after) { 194 if (s != mBase) 195 return; 196 197 CharSequence text = mDisplay; 198 int len = text.length(); 199 200 // seek back to the start of the paragraph 201 202 int find = TextUtils.lastIndexOf(text, '\n', where - 1); 203 if (find < 0) 204 find = 0; 205 else 206 find = find + 1; 207 208 { 209 int diff = where - find; 210 before += diff; 211 after += diff; 212 where -= diff; 213 } 214 215 // seek forward to the end of the paragraph 216 217 int look = TextUtils.indexOf(text, '\n', where + after); 218 if (look < 0) 219 look = len; 220 else 221 look++; // we want the index after the \n 222 223 int change = look - (where + after); 224 before += change; 225 after += change; 226 227 // seek further out to cover anything that is forced to wrap together 228 229 if (text instanceof Spanned) { 230 Spanned sp = (Spanned) text; 231 boolean again; 232 233 do { 234 again = false; 235 236 Object[] force = sp.getSpans(where, where + after, 237 WrapTogetherSpan.class); 238 239 for (int i = 0; i < force.length; i++) { 240 int st = sp.getSpanStart(force[i]); 241 int en = sp.getSpanEnd(force[i]); 242 243 if (st < where) { 244 again = true; 245 246 int diff = where - st; 247 before += diff; 248 after += diff; 249 where -= diff; 250 } 251 252 if (en > where + after) { 253 again = true; 254 255 int diff = en - (where + after); 256 before += diff; 257 after += diff; 258 } 259 } 260 } while (again); 261 } 262 263 // find affected region of old layout 264 265 int startline = getLineForOffset(where); 266 int startv = getLineTop(startline); 267 268 int endline = getLineForOffset(where + before); 269 if (where + after == len) 270 endline = getLineCount(); 271 int endv = getLineTop(endline); 272 boolean islast = (endline == getLineCount()); 273 274 // generate new layout for affected text 275 276 StaticLayout reflowed; 277 StaticLayout.Builder b; 278 279 synchronized (sLock) { 280 reflowed = sStaticLayout; 281 b = sBuilder; 282 sStaticLayout = null; 283 sBuilder = null; 284 } 285 286 if (reflowed == null) { 287 reflowed = new StaticLayout(null); 288 b = StaticLayout.Builder.obtain(text, where, where + after, getPaint(), getWidth()); 289 } 290 291 b.setText(text, where, where + after) 292 .setPaint(getPaint()) 293 .setWidth(getWidth()) 294 .setTextDirection(getTextDirectionHeuristic()) 295 .setLineSpacing(getSpacingAdd(), getSpacingMultiplier()) 296 .setEllipsizedWidth(mEllipsizedWidth) 297 .setEllipsize(mEllipsizeAt) 298 .setBreakStrategy(mBreakStrategy) 299 .setHyphenationFrequency(mHyphenationFrequency); 300 reflowed.generate(b, false, true); 301 int n = reflowed.getLineCount(); 302 303 // If the new layout has a blank line at the end, but it is not 304 // the very end of the buffer, then we already have a line that 305 // starts there, so disregard the blank line. 306 307 if (where + after != len && reflowed.getLineStart(n - 1) == where + after) 308 n--; 309 310 // remove affected lines from old layout 311 mInts.deleteAt(startline, endline - startline); 312 mObjects.deleteAt(startline, endline - startline); 313 314 // adjust offsets in layout for new height and offsets 315 316 int ht = reflowed.getLineTop(n); 317 int toppad = 0, botpad = 0; 318 319 if (mIncludePad && startline == 0) { 320 toppad = reflowed.getTopPadding(); 321 mTopPadding = toppad; 322 ht -= toppad; 323 } 324 if (mIncludePad && islast) { 325 botpad = reflowed.getBottomPadding(); 326 mBottomPadding = botpad; 327 ht += botpad; 328 } 329 330 mInts.adjustValuesBelow(startline, START, after - before); 331 mInts.adjustValuesBelow(startline, TOP, startv - endv + ht); 332 333 // insert new layout 334 335 int[] ints; 336 337 if (mEllipsize) { 338 ints = new int[COLUMNS_ELLIPSIZE]; 339 ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED; 340 } else { 341 ints = new int[COLUMNS_NORMAL]; 342 } 343 344 Directions[] objects = new Directions[1]; 345 346 for (int i = 0; i < n; i++) { 347 ints[START] = reflowed.getLineStart(i) | 348 (reflowed.getParagraphDirection(i) << DIR_SHIFT) | 349 (reflowed.getLineContainsTab(i) ? TAB_MASK : 0); 350 351 int top = reflowed.getLineTop(i) + startv; 352 if (i > 0) 353 top -= toppad; 354 ints[TOP] = top; 355 356 int desc = reflowed.getLineDescent(i); 357 if (i == n - 1) 358 desc += botpad; 359 360 ints[DESCENT] = desc; 361 objects[0] = reflowed.getLineDirections(i); 362 363 ints[HYPHEN] = reflowed.getHyphen(i); 364 365 if (mEllipsize) { 366 ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i); 367 ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i); 368 } 369 370 mInts.insertAt(startline + i, ints); 371 mObjects.insertAt(startline + i, objects); 372 } 373 374 updateBlocks(startline, endline - 1, n); 375 376 b.finish(); 377 synchronized (sLock) { 378 sStaticLayout = reflowed; 379 sBuilder = b; 380 } 381 } 382 383 /** 384 * Create the initial block structure, cutting the text into blocks of at least 385 * BLOCK_MINIMUM_CHARACTER_SIZE characters, aligned on the ends of paragraphs. 386 */ 387 private void createBlocks() { 388 int offset = BLOCK_MINIMUM_CHARACTER_LENGTH; 389 mNumberOfBlocks = 0; 390 final CharSequence text = mDisplay; 391 392 while (true) { 393 offset = TextUtils.indexOf(text, '\n', offset); 394 if (offset < 0) { 395 addBlockAtOffset(text.length()); 396 break; 397 } else { 398 addBlockAtOffset(offset); 399 offset += BLOCK_MINIMUM_CHARACTER_LENGTH; 400 } 401 } 402 403 // mBlockIndices and mBlockEndLines should have the same length 404 mBlockIndices = new int[mBlockEndLines.length]; 405 for (int i = 0; i < mBlockEndLines.length; i++) { 406 mBlockIndices[i] = INVALID_BLOCK_INDEX; 407 } 408 } 409 410 /** 411 * Create a new block, ending at the specified character offset. 412 * A block will actually be created only if has at least one line, i.e. this offset is 413 * not on the end line of the previous block. 414 */ 415 private void addBlockAtOffset(int offset) { 416 final int line = getLineForOffset(offset); 417 418 if (mBlockEndLines == null) { 419 // Initial creation of the array, no test on previous block ending line 420 mBlockEndLines = ArrayUtils.newUnpaddedIntArray(1); 421 mBlockEndLines[mNumberOfBlocks] = line; 422 mNumberOfBlocks++; 423 return; 424 } 425 426 final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1]; 427 if (line > previousBlockEndLine) { 428 mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line); 429 mNumberOfBlocks++; 430 } 431 } 432 433 /** 434 * This method is called every time the layout is reflowed after an edition. 435 * It updates the internal block data structure. The text is split in blocks 436 * of contiguous lines, with at least one block for the entire text. 437 * When a range of lines is edited, new blocks (from 0 to 3 depending on the 438 * overlap structure) will replace the set of overlapping blocks. 439 * Blocks are listed in order and are represented by their ending line number. 440 * An index is associated to each block (which will be used by display lists), 441 * this class simply invalidates the index of blocks overlapping a modification. 442 * 443 * This method is package private and not private so that it can be tested. 444 * 445 * @param startLine the first line of the range of modified lines 446 * @param endLine the last line of the range, possibly equal to startLine, lower 447 * than getLineCount() 448 * @param newLineCount the number of lines that will replace the range, possibly 0 449 * 450 * @hide 451 */ 452 void updateBlocks(int startLine, int endLine, int newLineCount) { 453 if (mBlockEndLines == null) { 454 createBlocks(); 455 return; 456 } 457 458 int firstBlock = -1; 459 int lastBlock = -1; 460 for (int i = 0; i < mNumberOfBlocks; i++) { 461 if (mBlockEndLines[i] >= startLine) { 462 firstBlock = i; 463 break; 464 } 465 } 466 for (int i = firstBlock; i < mNumberOfBlocks; i++) { 467 if (mBlockEndLines[i] >= endLine) { 468 lastBlock = i; 469 break; 470 } 471 } 472 final int lastBlockEndLine = mBlockEndLines[lastBlock]; 473 474 boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 : 475 mBlockEndLines[firstBlock - 1] + 1); 476 boolean createBlock = newLineCount > 0; 477 boolean createBlockAfter = endLine < mBlockEndLines[lastBlock]; 478 479 int numAddedBlocks = 0; 480 if (createBlockBefore) numAddedBlocks++; 481 if (createBlock) numAddedBlocks++; 482 if (createBlockAfter) numAddedBlocks++; 483 484 final int numRemovedBlocks = lastBlock - firstBlock + 1; 485 final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks; 486 487 if (newNumberOfBlocks == 0) { 488 // Even when text is empty, there is actually one line and hence one block 489 mBlockEndLines[0] = 0; 490 mBlockIndices[0] = INVALID_BLOCK_INDEX; 491 mNumberOfBlocks = 1; 492 return; 493 } 494 495 if (newNumberOfBlocks > mBlockEndLines.length) { 496 int[] blockEndLines = ArrayUtils.newUnpaddedIntArray( 497 Math.max(mBlockEndLines.length * 2, newNumberOfBlocks)); 498 int[] blockIndices = new int[blockEndLines.length]; 499 System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock); 500 System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock); 501 System.arraycopy(mBlockEndLines, lastBlock + 1, 502 blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 503 System.arraycopy(mBlockIndices, lastBlock + 1, 504 blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 505 mBlockEndLines = blockEndLines; 506 mBlockIndices = blockIndices; 507 } else { 508 System.arraycopy(mBlockEndLines, lastBlock + 1, 509 mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 510 System.arraycopy(mBlockIndices, lastBlock + 1, 511 mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 512 } 513 514 mNumberOfBlocks = newNumberOfBlocks; 515 int newFirstChangedBlock; 516 final int deltaLines = newLineCount - (endLine - startLine + 1); 517 if (deltaLines != 0) { 518 // Display list whose index is >= mIndexFirstChangedBlock is valid 519 // but it needs to update its drawing location. 520 newFirstChangedBlock = firstBlock + numAddedBlocks; 521 for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) { 522 mBlockEndLines[i] += deltaLines; 523 } 524 } else { 525 newFirstChangedBlock = mNumberOfBlocks; 526 } 527 mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock); 528 529 int blockIndex = firstBlock; 530 if (createBlockBefore) { 531 mBlockEndLines[blockIndex] = startLine - 1; 532 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX; 533 blockIndex++; 534 } 535 536 if (createBlock) { 537 mBlockEndLines[blockIndex] = startLine + newLineCount - 1; 538 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX; 539 blockIndex++; 540 } 541 542 if (createBlockAfter) { 543 mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines; 544 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX; 545 } 546 } 547 548 /** 549 * This package private method is used for test purposes only 550 * @hide 551 */ 552 void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks) { 553 mBlockEndLines = new int[blockEndLines.length]; 554 mBlockIndices = new int[blockIndices.length]; 555 System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length); 556 System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length); 557 mNumberOfBlocks = numberOfBlocks; 558 } 559 560 /** 561 * @hide 562 */ 563 public int[] getBlockEndLines() { 564 return mBlockEndLines; 565 } 566 567 /** 568 * @hide 569 */ 570 public int[] getBlockIndices() { 571 return mBlockIndices; 572 } 573 574 /** 575 * @hide 576 */ 577 public int getNumberOfBlocks() { 578 return mNumberOfBlocks; 579 } 580 581 /** 582 * @hide 583 */ 584 public int getIndexFirstChangedBlock() { 585 return mIndexFirstChangedBlock; 586 } 587 588 /** 589 * @hide 590 */ 591 public void setIndexFirstChangedBlock(int i) { 592 mIndexFirstChangedBlock = i; 593 } 594 595 @Override 596 public int getLineCount() { 597 return mInts.size() - 1; 598 } 599 600 @Override 601 public int getLineTop(int line) { 602 return mInts.getValue(line, TOP); 603 } 604 605 @Override 606 public int getLineDescent(int line) { 607 return mInts.getValue(line, DESCENT); 608 } 609 610 @Override 611 public int getLineStart(int line) { 612 return mInts.getValue(line, START) & START_MASK; 613 } 614 615 @Override 616 public boolean getLineContainsTab(int line) { 617 return (mInts.getValue(line, TAB) & TAB_MASK) != 0; 618 } 619 620 @Override 621 public int getParagraphDirection(int line) { 622 return mInts.getValue(line, DIR) >> DIR_SHIFT; 623 } 624 625 @Override 626 public final Directions getLineDirections(int line) { 627 return mObjects.getValue(line, 0); 628 } 629 630 @Override 631 public int getTopPadding() { 632 return mTopPadding; 633 } 634 635 @Override 636 public int getBottomPadding() { 637 return mBottomPadding; 638 } 639 640 /** 641 * @hide 642 */ 643 @Override 644 public int getHyphen(int line) { 645 return mInts.getValue(line, HYPHEN); 646 } 647 648 @Override 649 public int getEllipsizedWidth() { 650 return mEllipsizedWidth; 651 } 652 653 private static class ChangeWatcher implements TextWatcher, SpanWatcher { 654 public ChangeWatcher(DynamicLayout layout) { 655 mLayout = new WeakReference<DynamicLayout>(layout); 656 } 657 658 private void reflow(CharSequence s, int where, int before, int after) { 659 DynamicLayout ml = mLayout.get(); 660 661 if (ml != null) 662 ml.reflow(s, where, before, after); 663 else if (s instanceof Spannable) 664 ((Spannable) s).removeSpan(this); 665 } 666 667 public void beforeTextChanged(CharSequence s, int where, int before, int after) { 668 // Intentionally empty 669 } 670 671 public void onTextChanged(CharSequence s, int where, int before, int after) { 672 reflow(s, where, before, after); 673 } 674 675 public void afterTextChanged(Editable s) { 676 // Intentionally empty 677 } 678 679 public void onSpanAdded(Spannable s, Object o, int start, int end) { 680 if (o instanceof UpdateLayout) 681 reflow(s, start, end - start, end - start); 682 } 683 684 public void onSpanRemoved(Spannable s, Object o, int start, int end) { 685 if (o instanceof UpdateLayout) 686 reflow(s, start, end - start, end - start); 687 } 688 689 public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) { 690 if (o instanceof UpdateLayout) { 691 reflow(s, start, end - start, end - start); 692 reflow(s, nstart, nend - nstart, nend - nstart); 693 } 694 } 695 696 private WeakReference<DynamicLayout> mLayout; 697 } 698 699 @Override 700 public int getEllipsisStart(int line) { 701 if (mEllipsizeAt == null) { 702 return 0; 703 } 704 705 return mInts.getValue(line, ELLIPSIS_START); 706 } 707 708 @Override 709 public int getEllipsisCount(int line) { 710 if (mEllipsizeAt == null) { 711 return 0; 712 } 713 714 return mInts.getValue(line, ELLIPSIS_COUNT); 715 } 716 717 private CharSequence mBase; 718 private CharSequence mDisplay; 719 private ChangeWatcher mWatcher; 720 private boolean mIncludePad; 721 private boolean mEllipsize; 722 private int mEllipsizedWidth; 723 private TextUtils.TruncateAt mEllipsizeAt; 724 private int mBreakStrategy; 725 private int mHyphenationFrequency; 726 727 private PackedIntVector mInts; 728 private PackedObjectVector<Directions> mObjects; 729 730 /** 731 * Value used in mBlockIndices when a block has been created or recycled and indicating that its 732 * display list needs to be re-created. 733 * @hide 734 */ 735 public static final int INVALID_BLOCK_INDEX = -1; 736 // Stores the line numbers of the last line of each block (inclusive) 737 private int[] mBlockEndLines; 738 // The indices of this block's display list in TextView's internal display list array or 739 // INVALID_BLOCK_INDEX if this block has been invalidated during an edition 740 private int[] mBlockIndices; 741 // Number of items actually currently being used in the above 2 arrays 742 private int mNumberOfBlocks; 743 // The first index of the blocks whose locations are changed 744 private int mIndexFirstChangedBlock; 745 746 private int mTopPadding, mBottomPadding; 747 748 private static StaticLayout sStaticLayout = null; 749 private static StaticLayout.Builder sBuilder = null; 750 751 private static final Object[] sLock = new Object[0]; 752 753 private static final int START = 0; 754 private static final int DIR = START; 755 private static final int TAB = START; 756 private static final int TOP = 1; 757 private static final int DESCENT = 2; 758 private static final int HYPHEN = 3; 759 private static final int COLUMNS_NORMAL = 4; 760 761 private static final int ELLIPSIS_START = 4; 762 private static final int ELLIPSIS_COUNT = 5; 763 private static final int COLUMNS_ELLIPSIZE = 6; 764 765 private static final int START_MASK = 0x1FFFFFFF; 766 private static final int DIR_SHIFT = 30; 767 private static final int TAB_MASK = 0x20000000; 768 769 private static final int ELLIPSIS_UNDEFINED = 0x80000000; 770} 771