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