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