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.graphics.Rect;
21import android.text.style.ReplacementSpan;
22import android.text.style.UpdateLayout;
23import android.text.style.WrapTogetherSpan;
24import android.util.ArraySet;
25
26import com.android.internal.annotations.VisibleForTesting;
27import com.android.internal.util.ArrayUtils;
28import com.android.internal.util.GrowingArrayUtils;
29
30import 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 */
40public 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