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