1/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.text;
18
19import android.graphics.Paint;
20import android.text.style.UpdateLayout;
21import android.text.style.WrapTogetherSpan;
22
23import com.android.internal.util.ArrayUtils;
24import com.android.internal.util.GrowingArrayUtils;
25
26import java.lang.ref.WeakReference;
27
28/**
29 * DynamicLayout is a text layout that updates itself as the text is edited.
30 * <p>This is used by widgets to control text layout. You should not need
31 * to use this class directly unless you are implementing your own widget
32 * or custom display object, or need to call
33 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
34 *  Canvas.drawText()} directly.</p>
35 */
36public class DynamicLayout extends Layout
37{
38    private static final int PRIORITY = 128;
39    private static final int BLOCK_MINIMUM_CHARACTER_LENGTH = 400;
40
41    /**
42     * Make a layout for the specified text that will be updated as
43     * the text is changed.
44     */
45    public DynamicLayout(CharSequence base,
46                         TextPaint paint,
47                         int width, Alignment align,
48                         float spacingmult, float spacingadd,
49                         boolean includepad) {
50        this(base, base, paint, width, align, spacingmult, spacingadd,
51             includepad);
52    }
53
54    /**
55     * Make a layout for the transformed text (password transformation
56     * being the primary example of a transformation)
57     * that will be updated as the base text is changed.
58     */
59    public DynamicLayout(CharSequence base, CharSequence display,
60                         TextPaint paint,
61                         int width, Alignment align,
62                         float spacingmult, float spacingadd,
63                         boolean includepad) {
64        this(base, display, paint, width, align, spacingmult, spacingadd,
65             includepad, null, 0);
66    }
67
68    /**
69     * Make a layout for the transformed text (password transformation
70     * being the primary example of a transformation)
71     * that will be updated as the base text is changed.
72     * If ellipsize is non-null, the Layout will ellipsize the text
73     * down to ellipsizedWidth.
74     */
75    public DynamicLayout(CharSequence base, CharSequence display,
76                         TextPaint paint,
77                         int width, Alignment align,
78                         float spacingmult, float spacingadd,
79                         boolean includepad,
80                         TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
81        this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
82                spacingmult, spacingadd, includepad,
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