Home | History | Annotate | Download | only in text
      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 import com.android.internal.util.GrowingArrayUtils;
     25 
     26 import 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  */
     36 public 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 
    274         synchronized (sLock) {
    275             reflowed = sStaticLayout;
    276             sStaticLayout = null;
    277         }
    278 
    279         if (reflowed == null) {
    280             reflowed = new StaticLayout(null);
    281         } else {
    282             reflowed.prepare();
    283         }
    284 
    285         reflowed.generate(text, where, where + after,
    286                 getPaint(), getWidth(), getTextDirectionHeuristic(), getSpacingMultiplier(),
    287                 getSpacingAdd(), false,
    288                 true, mEllipsizedWidth, mEllipsizeAt);
    289         int n = reflowed.getLineCount();
    290 
    291         // If the new layout has a blank line at the end, but it is not
    292         // the very end of the buffer, then we already have a line that
    293         // starts there, so disregard the blank line.
    294 
    295         if (where + after != len && reflowed.getLineStart(n - 1) == where + after)
    296             n--;
    297 
    298         // remove affected lines from old layout
    299         mInts.deleteAt(startline, endline - startline);
    300         mObjects.deleteAt(startline, endline - startline);
    301 
    302         // adjust offsets in layout for new height and offsets
    303 
    304         int ht = reflowed.getLineTop(n);
    305         int toppad = 0, botpad = 0;
    306 
    307         if (mIncludePad && startline == 0) {
    308             toppad = reflowed.getTopPadding();
    309             mTopPadding = toppad;
    310             ht -= toppad;
    311         }
    312         if (mIncludePad && islast) {
    313             botpad = reflowed.getBottomPadding();
    314             mBottomPadding = botpad;
    315             ht += botpad;
    316         }
    317 
    318         mInts.adjustValuesBelow(startline, START, after - before);
    319         mInts.adjustValuesBelow(startline, TOP, startv - endv + ht);
    320 
    321         // insert new layout
    322 
    323         int[] ints;
    324 
    325         if (mEllipsize) {
    326             ints = new int[COLUMNS_ELLIPSIZE];
    327             ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
    328         } else {
    329             ints = new int[COLUMNS_NORMAL];
    330         }
    331 
    332         Directions[] objects = new Directions[1];
    333 
    334         for (int i = 0; i < n; i++) {
    335             ints[START] = reflowed.getLineStart(i) |
    336                           (reflowed.getParagraphDirection(i) << DIR_SHIFT) |
    337                           (reflowed.getLineContainsTab(i) ? TAB_MASK : 0);
    338 
    339             int top = reflowed.getLineTop(i) + startv;
    340             if (i > 0)
    341                 top -= toppad;
    342             ints[TOP] = top;
    343 
    344             int desc = reflowed.getLineDescent(i);
    345             if (i == n - 1)
    346                 desc += botpad;
    347 
    348             ints[DESCENT] = desc;
    349             objects[0] = reflowed.getLineDirections(i);
    350 
    351             if (mEllipsize) {
    352                 ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
    353                 ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i);
    354             }
    355 
    356             mInts.insertAt(startline + i, ints);
    357             mObjects.insertAt(startline + i, objects);
    358         }
    359 
    360         updateBlocks(startline, endline - 1, n);
    361 
    362         synchronized (sLock) {
    363             sStaticLayout = reflowed;
    364             reflowed.finish();
    365         }
    366     }
    367 
    368     /**
    369      * Create the initial block structure, cutting the text into blocks of at least
    370      * BLOCK_MINIMUM_CHARACTER_SIZE characters, aligned on the ends of paragraphs.
    371      */
    372     private void createBlocks() {
    373         int offset = BLOCK_MINIMUM_CHARACTER_LENGTH;
    374         mNumberOfBlocks = 0;
    375         final CharSequence text = mDisplay;
    376 
    377         while (true) {
    378             offset = TextUtils.indexOf(text, '\n', offset);
    379             if (offset < 0) {
    380                 addBlockAtOffset(text.length());
    381                 break;
    382             } else {
    383                 addBlockAtOffset(offset);
    384                 offset += BLOCK_MINIMUM_CHARACTER_LENGTH;
    385             }
    386         }
    387 
    388         // mBlockIndices and mBlockEndLines should have the same length
    389         mBlockIndices = new int[mBlockEndLines.length];
    390         for (int i = 0; i < mBlockEndLines.length; i++) {
    391             mBlockIndices[i] = INVALID_BLOCK_INDEX;
    392         }
    393     }
    394 
    395     /**
    396      * Create a new block, ending at the specified character offset.
    397      * A block will actually be created only if has at least one line, i.e. this offset is
    398      * not on the end line of the previous block.
    399      */
    400     private void addBlockAtOffset(int offset) {
    401         final int line = getLineForOffset(offset);
    402 
    403         if (mBlockEndLines == null) {
    404             // Initial creation of the array, no test on previous block ending line
    405             mBlockEndLines = ArrayUtils.newUnpaddedIntArray(1);
    406             mBlockEndLines[mNumberOfBlocks] = line;
    407             mNumberOfBlocks++;
    408             return;
    409         }
    410 
    411         final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1];
    412         if (line > previousBlockEndLine) {
    413             mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line);
    414             mNumberOfBlocks++;
    415         }
    416     }
    417 
    418     /**
    419      * This method is called every time the layout is reflowed after an edition.
    420      * It updates the internal block data structure. The text is split in blocks
    421      * of contiguous lines, with at least one block for the entire text.
    422      * When a range of lines is edited, new blocks (from 0 to 3 depending on the
    423      * overlap structure) will replace the set of overlapping blocks.
    424      * Blocks are listed in order and are represented by their ending line number.
    425      * An index is associated to each block (which will be used by display lists),
    426      * this class simply invalidates the index of blocks overlapping a modification.
    427      *
    428      * This method is package private and not private so that it can be tested.
    429      *
    430      * @param startLine the first line of the range of modified lines
    431      * @param endLine the last line of the range, possibly equal to startLine, lower
    432      * than getLineCount()
    433      * @param newLineCount the number of lines that will replace the range, possibly 0
    434      *
    435      * @hide
    436      */
    437     void updateBlocks(int startLine, int endLine, int newLineCount) {
    438         if (mBlockEndLines == null) {
    439             createBlocks();
    440             return;
    441         }
    442 
    443         int firstBlock = -1;
    444         int lastBlock = -1;
    445         for (int i = 0; i < mNumberOfBlocks; i++) {
    446             if (mBlockEndLines[i] >= startLine) {
    447                 firstBlock = i;
    448                 break;
    449             }
    450         }
    451         for (int i = firstBlock; i < mNumberOfBlocks; i++) {
    452             if (mBlockEndLines[i] >= endLine) {
    453                 lastBlock = i;
    454                 break;
    455             }
    456         }
    457         final int lastBlockEndLine = mBlockEndLines[lastBlock];
    458 
    459         boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
    460                 mBlockEndLines[firstBlock - 1] + 1);
    461         boolean createBlock = newLineCount > 0;
    462         boolean createBlockAfter = endLine < mBlockEndLines[lastBlock];
    463 
    464         int numAddedBlocks = 0;
    465         if (createBlockBefore) numAddedBlocks++;
    466         if (createBlock) numAddedBlocks++;
    467         if (createBlockAfter) numAddedBlocks++;
    468 
    469         final int numRemovedBlocks = lastBlock - firstBlock + 1;
    470         final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks;
    471 
    472         if (newNumberOfBlocks == 0) {
    473             // Even when text is empty, there is actually one line and hence one block
    474             mBlockEndLines[0] = 0;
    475             mBlockIndices[0] = INVALID_BLOCK_INDEX;
    476             mNumberOfBlocks = 1;
    477             return;
    478         }
    479 
    480         if (newNumberOfBlocks > mBlockEndLines.length) {
    481             int[] blockEndLines = ArrayUtils.newUnpaddedIntArray(
    482                     Math.max(mBlockEndLines.length * 2, newNumberOfBlocks));
    483             int[] blockIndices = new int[blockEndLines.length];
    484             System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock);
    485             System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock);
    486             System.arraycopy(mBlockEndLines, lastBlock + 1,
    487                     blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
    488             System.arraycopy(mBlockIndices, lastBlock + 1,
    489                     blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
    490             mBlockEndLines = blockEndLines;
    491             mBlockIndices = blockIndices;
    492         } else {
    493             System.arraycopy(mBlockEndLines, lastBlock + 1,
    494                     mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
    495             System.arraycopy(mBlockIndices, lastBlock + 1,
    496                     mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
    497         }
    498 
    499         mNumberOfBlocks = newNumberOfBlocks;
    500         int newFirstChangedBlock;
    501         final int deltaLines = newLineCount - (endLine - startLine + 1);
    502         if (deltaLines != 0) {
    503             // Display list whose index is >= mIndexFirstChangedBlock is valid
    504             // but it needs to update its drawing location.
    505             newFirstChangedBlock = firstBlock + numAddedBlocks;
    506             for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) {
    507                 mBlockEndLines[i] += deltaLines;
    508             }
    509         } else {
    510             newFirstChangedBlock = mNumberOfBlocks;
    511         }
    512         mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock);
    513 
    514         int blockIndex = firstBlock;
    515         if (createBlockBefore) {
    516             mBlockEndLines[blockIndex] = startLine - 1;
    517             mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
    518             blockIndex++;
    519         }
    520 
    521         if (createBlock) {
    522             mBlockEndLines[blockIndex] = startLine + newLineCount - 1;
    523             mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
    524             blockIndex++;
    525         }
    526 
    527         if (createBlockAfter) {
    528             mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines;
    529             mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
    530         }
    531     }
    532 
    533     /**
    534      * This package private method is used for test purposes only
    535      * @hide
    536      */
    537     void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks) {
    538         mBlockEndLines = new int[blockEndLines.length];
    539         mBlockIndices = new int[blockIndices.length];
    540         System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length);
    541         System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length);
    542         mNumberOfBlocks = numberOfBlocks;
    543     }
    544 
    545     /**
    546      * @hide
    547      */
    548     public int[] getBlockEndLines() {
    549         return mBlockEndLines;
    550     }
    551 
    552     /**
    553      * @hide
    554      */
    555     public int[] getBlockIndices() {
    556         return mBlockIndices;
    557     }
    558 
    559     /**
    560      * @hide
    561      */
    562     public int getNumberOfBlocks() {
    563         return mNumberOfBlocks;
    564     }
    565 
    566     /**
    567      * @hide
    568      */
    569     public int getIndexFirstChangedBlock() {
    570         return mIndexFirstChangedBlock;
    571     }
    572 
    573     /**
    574      * @hide
    575      */
    576     public void setIndexFirstChangedBlock(int i) {
    577         mIndexFirstChangedBlock = i;
    578     }
    579 
    580     @Override
    581     public int getLineCount() {
    582         return mInts.size() - 1;
    583     }
    584 
    585     @Override
    586     public int getLineTop(int line) {
    587         return mInts.getValue(line, TOP);
    588     }
    589 
    590     @Override
    591     public int getLineDescent(int line) {
    592         return mInts.getValue(line, DESCENT);
    593     }
    594 
    595     @Override
    596     public int getLineStart(int line) {
    597         return mInts.getValue(line, START) & START_MASK;
    598     }
    599 
    600     @Override
    601     public boolean getLineContainsTab(int line) {
    602         return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
    603     }
    604 
    605     @Override
    606     public int getParagraphDirection(int line) {
    607         return mInts.getValue(line, DIR) >> DIR_SHIFT;
    608     }
    609 
    610     @Override
    611     public final Directions getLineDirections(int line) {
    612         return mObjects.getValue(line, 0);
    613     }
    614 
    615     @Override
    616     public int getTopPadding() {
    617         return mTopPadding;
    618     }
    619 
    620     @Override
    621     public int getBottomPadding() {
    622         return mBottomPadding;
    623     }
    624 
    625     @Override
    626     public int getEllipsizedWidth() {
    627         return mEllipsizedWidth;
    628     }
    629 
    630     private static class ChangeWatcher implements TextWatcher, SpanWatcher {
    631         public ChangeWatcher(DynamicLayout layout) {
    632             mLayout = new WeakReference<DynamicLayout>(layout);
    633         }
    634 
    635         private void reflow(CharSequence s, int where, int before, int after) {
    636             DynamicLayout ml = mLayout.get();
    637 
    638             if (ml != null)
    639                 ml.reflow(s, where, before, after);
    640             else if (s instanceof Spannable)
    641                 ((Spannable) s).removeSpan(this);
    642         }
    643 
    644         public void beforeTextChanged(CharSequence s, int where, int before, int after) {
    645             // Intentionally empty
    646         }
    647 
    648         public void onTextChanged(CharSequence s, int where, int before, int after) {
    649             reflow(s, where, before, after);
    650         }
    651 
    652         public void afterTextChanged(Editable s) {
    653             // Intentionally empty
    654         }
    655 
    656         public void onSpanAdded(Spannable s, Object o, int start, int end) {
    657             if (o instanceof UpdateLayout)
    658                 reflow(s, start, end - start, end - start);
    659         }
    660 
    661         public void onSpanRemoved(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 onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) {
    667             if (o instanceof UpdateLayout) {
    668                 reflow(s, start, end - start, end - start);
    669                 reflow(s, nstart, nend - nstart, nend - nstart);
    670             }
    671         }
    672 
    673         private WeakReference<DynamicLayout> mLayout;
    674     }
    675 
    676     @Override
    677     public int getEllipsisStart(int line) {
    678         if (mEllipsizeAt == null) {
    679             return 0;
    680         }
    681 
    682         return mInts.getValue(line, ELLIPSIS_START);
    683     }
    684 
    685     @Override
    686     public int getEllipsisCount(int line) {
    687         if (mEllipsizeAt == null) {
    688             return 0;
    689         }
    690 
    691         return mInts.getValue(line, ELLIPSIS_COUNT);
    692     }
    693 
    694     private CharSequence mBase;
    695     private CharSequence mDisplay;
    696     private ChangeWatcher mWatcher;
    697     private boolean mIncludePad;
    698     private boolean mEllipsize;
    699     private int mEllipsizedWidth;
    700     private TextUtils.TruncateAt mEllipsizeAt;
    701 
    702     private PackedIntVector mInts;
    703     private PackedObjectVector<Directions> mObjects;
    704 
    705     /**
    706      * Value used in mBlockIndices when a block has been created or recycled and indicating that its
    707      * display list needs to be re-created.
    708      * @hide
    709      */
    710     public static final int INVALID_BLOCK_INDEX = -1;
    711     // Stores the line numbers of the last line of each block (inclusive)
    712     private int[] mBlockEndLines;
    713     // The indices of this block's display list in TextView's internal display list array or
    714     // INVALID_BLOCK_INDEX if this block has been invalidated during an edition
    715     private int[] mBlockIndices;
    716     // Number of items actually currently being used in the above 2 arrays
    717     private int mNumberOfBlocks;
    718     // The first index of the blocks whose locations are changed
    719     private int mIndexFirstChangedBlock;
    720 
    721     private int mTopPadding, mBottomPadding;
    722 
    723     private static StaticLayout sStaticLayout = new StaticLayout(null);
    724 
    725     private static final Object[] sLock = new Object[0];
    726 
    727     private static final int START = 0;
    728     private static final int DIR = START;
    729     private static final int TAB = START;
    730     private static final int TOP = 1;
    731     private static final int DESCENT = 2;
    732     private static final int COLUMNS_NORMAL = 3;
    733 
    734     private static final int ELLIPSIS_START = 3;
    735     private static final int ELLIPSIS_COUNT = 4;
    736     private static final int COLUMNS_ELLIPSIZE = 5;
    737 
    738     private static final int START_MASK = 0x1FFFFFFF;
    739     private static final int DIR_SHIFT  = 30;
    740     private static final int TAB_MASK   = 0x20000000;
    741 
    742     private static final int ELLIPSIS_UNDEFINED = 0x80000000;
    743 }
    744