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