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