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