DynamicLayout.java revision fb0b2dc754fe54269dc8aac86b6e8c1458f96013
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                .setAddLastLineLineSpacing(!islast);
308
309        reflowed.generate(b, false /*includepad*/, true /*trackpad*/);
310        int n = reflowed.getLineCount();
311        // If the new layout has a blank line at the end, but it is not
312        // the very end of the buffer, then we already have a line that
313        // starts there, so disregard the blank line.
314
315        if (where + after != len && reflowed.getLineStart(n - 1) == where + after)
316            n--;
317
318        // remove affected lines from old layout
319        mInts.deleteAt(startline, endline - startline);
320        mObjects.deleteAt(startline, endline - startline);
321
322        // adjust offsets in layout for new height and offsets
323
324        int ht = reflowed.getLineTop(n);
325        int toppad = 0, botpad = 0;
326
327        if (mIncludePad && startline == 0) {
328            toppad = reflowed.getTopPadding();
329            mTopPadding = toppad;
330            ht -= toppad;
331        }
332        if (mIncludePad && islast) {
333            botpad = reflowed.getBottomPadding();
334            mBottomPadding = botpad;
335            ht += botpad;
336        }
337
338        mInts.adjustValuesBelow(startline, START, after - before);
339        mInts.adjustValuesBelow(startline, TOP, startv - endv + ht);
340
341        // insert new layout
342
343        int[] ints;
344
345        if (mEllipsize) {
346            ints = new int[COLUMNS_ELLIPSIZE];
347            ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
348        } else {
349            ints = new int[COLUMNS_NORMAL];
350        }
351
352        Directions[] objects = new Directions[1];
353
354        for (int i = 0; i < n; i++) {
355            final int start = reflowed.getLineStart(i);
356            ints[START] = start;
357            ints[DIR] |= reflowed.getParagraphDirection(i) << DIR_SHIFT;
358            ints[TAB] |= reflowed.getLineContainsTab(i) ? TAB_MASK : 0;
359
360            int top = reflowed.getLineTop(i) + startv;
361            if (i > 0)
362                top -= toppad;
363            ints[TOP] = top;
364
365            int desc = reflowed.getLineDescent(i);
366            if (i == n - 1)
367                desc += botpad;
368
369            ints[DESCENT] = desc;
370            ints[EXTRA] = reflowed.getLineExtra(i);
371            objects[0] = reflowed.getLineDirections(i);
372
373            final int end = (i == n - 1) ? where + after : reflowed.getLineStart(i + 1);
374            ints[HYPHEN] = reflowed.getHyphen(i) & HYPHEN_MASK;
375            ints[MAY_PROTRUDE_FROM_TOP_OR_BOTTOM] |=
376                    contentMayProtrudeFromLineTopOrBottom(text, start, end) ?
377                            MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK : 0;
378
379            if (mEllipsize) {
380                ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
381                ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i);
382            }
383
384            mInts.insertAt(startline + i, ints);
385            mObjects.insertAt(startline + i, objects);
386        }
387
388        updateBlocks(startline, endline - 1, n);
389
390        b.finish();
391        synchronized (sLock) {
392            sStaticLayout = reflowed;
393            sBuilder = b;
394        }
395    }
396
397    private boolean contentMayProtrudeFromLineTopOrBottom(CharSequence text, int start, int end) {
398        if (text instanceof Spanned) {
399            final Spanned spanned = (Spanned) text;
400            if (spanned.getSpans(start, end, ReplacementSpan.class).length > 0) {
401                return true;
402            }
403        }
404        // Spans other than ReplacementSpan can be ignored because line top and bottom are
405        // disjunction of all tops and bottoms, although it's not optimal.
406        final Paint paint = getPaint();
407        paint.getTextBounds(text, start, end, mTempRect);
408        final Paint.FontMetricsInt fm = paint.getFontMetricsInt();
409        return mTempRect.top < fm.top || mTempRect.bottom > fm.bottom;
410    }
411
412    /**
413     * Create the initial block structure, cutting the text into blocks of at least
414     * BLOCK_MINIMUM_CHARACTER_SIZE characters, aligned on the ends of paragraphs.
415     */
416    private void createBlocks() {
417        int offset = BLOCK_MINIMUM_CHARACTER_LENGTH;
418        mNumberOfBlocks = 0;
419        final CharSequence text = mDisplay;
420
421        while (true) {
422            offset = TextUtils.indexOf(text, '\n', offset);
423            if (offset < 0) {
424                addBlockAtOffset(text.length());
425                break;
426            } else {
427                addBlockAtOffset(offset);
428                offset += BLOCK_MINIMUM_CHARACTER_LENGTH;
429            }
430        }
431
432        // mBlockIndices and mBlockEndLines should have the same length
433        mBlockIndices = new int[mBlockEndLines.length];
434        for (int i = 0; i < mBlockEndLines.length; i++) {
435            mBlockIndices[i] = INVALID_BLOCK_INDEX;
436        }
437    }
438
439    /**
440     * @hide
441     */
442    public ArraySet<Integer> getBlocksAlwaysNeedToBeRedrawn() {
443        return mBlocksAlwaysNeedToBeRedrawn;
444    }
445
446    private void updateAlwaysNeedsToBeRedrawn(int blockIndex) {
447        int startLine = blockIndex == 0 ? 0 : (mBlockEndLines[blockIndex - 1] + 1);
448        int endLine = mBlockEndLines[blockIndex];
449        for (int i = startLine; i <= endLine; i++) {
450            if (getContentMayProtrudeFromTopOrBottom(i)) {
451                if (mBlocksAlwaysNeedToBeRedrawn == null) {
452                    mBlocksAlwaysNeedToBeRedrawn = new ArraySet<>();
453                }
454                mBlocksAlwaysNeedToBeRedrawn.add(blockIndex);
455                return;
456            }
457        }
458        if (mBlocksAlwaysNeedToBeRedrawn != null) {
459            mBlocksAlwaysNeedToBeRedrawn.remove(blockIndex);
460        }
461    }
462
463    /**
464     * Create a new block, ending at the specified character offset.
465     * A block will actually be created only if has at least one line, i.e. this offset is
466     * not on the end line of the previous block.
467     */
468    private void addBlockAtOffset(int offset) {
469        final int line = getLineForOffset(offset);
470        if (mBlockEndLines == null) {
471            // Initial creation of the array, no test on previous block ending line
472            mBlockEndLines = ArrayUtils.newUnpaddedIntArray(1);
473            mBlockEndLines[mNumberOfBlocks] = line;
474            updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
475            mNumberOfBlocks++;
476            return;
477        }
478
479        final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1];
480        if (line > previousBlockEndLine) {
481            mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line);
482            updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
483            mNumberOfBlocks++;
484        }
485    }
486
487    /**
488     * This method is called every time the layout is reflowed after an edition.
489     * It updates the internal block data structure. The text is split in blocks
490     * of contiguous lines, with at least one block for the entire text.
491     * When a range of lines is edited, new blocks (from 0 to 3 depending on the
492     * overlap structure) will replace the set of overlapping blocks.
493     * Blocks are listed in order and are represented by their ending line number.
494     * An index is associated to each block (which will be used by display lists),
495     * this class simply invalidates the index of blocks overlapping a modification.
496     *
497     * @param startLine the first line of the range of modified lines
498     * @param endLine the last line of the range, possibly equal to startLine, lower
499     * than getLineCount()
500     * @param newLineCount the number of lines that will replace the range, possibly 0
501     *
502     * @hide
503     */
504    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
505    public void updateBlocks(int startLine, int endLine, int newLineCount) {
506        if (mBlockEndLines == null) {
507            createBlocks();
508            return;
509        }
510
511        int firstBlock = -1;
512        int lastBlock = -1;
513        for (int i = 0; i < mNumberOfBlocks; i++) {
514            if (mBlockEndLines[i] >= startLine) {
515                firstBlock = i;
516                break;
517            }
518        }
519        for (int i = firstBlock; i < mNumberOfBlocks; i++) {
520            if (mBlockEndLines[i] >= endLine) {
521                lastBlock = i;
522                break;
523            }
524        }
525        final int lastBlockEndLine = mBlockEndLines[lastBlock];
526
527        boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
528                mBlockEndLines[firstBlock - 1] + 1);
529        boolean createBlock = newLineCount > 0;
530        boolean createBlockAfter = endLine < mBlockEndLines[lastBlock];
531
532        int numAddedBlocks = 0;
533        if (createBlockBefore) numAddedBlocks++;
534        if (createBlock) numAddedBlocks++;
535        if (createBlockAfter) numAddedBlocks++;
536
537        final int numRemovedBlocks = lastBlock - firstBlock + 1;
538        final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks;
539
540        if (newNumberOfBlocks == 0) {
541            // Even when text is empty, there is actually one line and hence one block
542            mBlockEndLines[0] = 0;
543            mBlockIndices[0] = INVALID_BLOCK_INDEX;
544            mNumberOfBlocks = 1;
545            return;
546        }
547
548        if (newNumberOfBlocks > mBlockEndLines.length) {
549            int[] blockEndLines = ArrayUtils.newUnpaddedIntArray(
550                    Math.max(mBlockEndLines.length * 2, newNumberOfBlocks));
551            int[] blockIndices = new int[blockEndLines.length];
552            System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock);
553            System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock);
554            System.arraycopy(mBlockEndLines, lastBlock + 1,
555                    blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
556            System.arraycopy(mBlockIndices, lastBlock + 1,
557                    blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
558            mBlockEndLines = blockEndLines;
559            mBlockIndices = blockIndices;
560        } else if (numAddedBlocks + numRemovedBlocks != 0) {
561            System.arraycopy(mBlockEndLines, lastBlock + 1,
562                    mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
563            System.arraycopy(mBlockIndices, lastBlock + 1,
564                    mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
565        }
566
567        if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) {
568            final ArraySet<Integer> set = new ArraySet<>();
569            for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) {
570                Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i);
571                if (block > firstBlock) {
572                    block += numAddedBlocks - numRemovedBlocks;
573                }
574                set.add(block);
575            }
576            mBlocksAlwaysNeedToBeRedrawn = set;
577        }
578
579        mNumberOfBlocks = newNumberOfBlocks;
580        int newFirstChangedBlock;
581        final int deltaLines = newLineCount - (endLine - startLine + 1);
582        if (deltaLines != 0) {
583            // Display list whose index is >= mIndexFirstChangedBlock is valid
584            // but it needs to update its drawing location.
585            newFirstChangedBlock = firstBlock + numAddedBlocks;
586            for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) {
587                mBlockEndLines[i] += deltaLines;
588            }
589        } else {
590            newFirstChangedBlock = mNumberOfBlocks;
591        }
592        mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock);
593
594        int blockIndex = firstBlock;
595        if (createBlockBefore) {
596            mBlockEndLines[blockIndex] = startLine - 1;
597            updateAlwaysNeedsToBeRedrawn(blockIndex);
598            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
599            blockIndex++;
600        }
601
602        if (createBlock) {
603            mBlockEndLines[blockIndex] = startLine + newLineCount - 1;
604            updateAlwaysNeedsToBeRedrawn(blockIndex);
605            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
606            blockIndex++;
607        }
608
609        if (createBlockAfter) {
610            mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines;
611            updateAlwaysNeedsToBeRedrawn(blockIndex);
612            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
613        }
614    }
615
616    /**
617     * This method is used for test purposes only.
618     * @hide
619     */
620    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
621    public void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks,
622            int totalLines) {
623        mBlockEndLines = new int[blockEndLines.length];
624        mBlockIndices = new int[blockIndices.length];
625        System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length);
626        System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length);
627        mNumberOfBlocks = numberOfBlocks;
628        while (mInts.size() < totalLines) {
629            mInts.insertAt(mInts.size(), new int[COLUMNS_NORMAL]);
630        }
631    }
632
633    /**
634     * @hide
635     */
636    public int[] getBlockEndLines() {
637        return mBlockEndLines;
638    }
639
640    /**
641     * @hide
642     */
643    public int[] getBlockIndices() {
644        return mBlockIndices;
645    }
646
647    /**
648     * @hide
649     */
650    public int getBlockIndex(int index) {
651        return mBlockIndices[index];
652    }
653
654    /**
655     * @hide
656     * @param index
657     */
658    public void setBlockIndex(int index, int blockIndex) {
659        mBlockIndices[index] = blockIndex;
660    }
661
662    /**
663     * @hide
664     */
665    public int getNumberOfBlocks() {
666        return mNumberOfBlocks;
667    }
668
669    /**
670     * @hide
671     */
672    public int getIndexFirstChangedBlock() {
673        return mIndexFirstChangedBlock;
674    }
675
676    /**
677     * @hide
678     */
679    public void setIndexFirstChangedBlock(int i) {
680        mIndexFirstChangedBlock = i;
681    }
682
683    @Override
684    public int getLineCount() {
685        return mInts.size() - 1;
686    }
687
688    @Override
689    public int getLineTop(int line) {
690        return mInts.getValue(line, TOP);
691    }
692
693    @Override
694    public int getLineDescent(int line) {
695        return mInts.getValue(line, DESCENT);
696    }
697
698    /**
699     * @hide
700     */
701    @Override
702    public int getLineExtra(int line) {
703        return mInts.getValue(line, EXTRA);
704    }
705
706    @Override
707    public int getLineStart(int line) {
708        return mInts.getValue(line, START) & START_MASK;
709    }
710
711    @Override
712    public boolean getLineContainsTab(int line) {
713        return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
714    }
715
716    @Override
717    public int getParagraphDirection(int line) {
718        return mInts.getValue(line, DIR) >> DIR_SHIFT;
719    }
720
721    @Override
722    public final Directions getLineDirections(int line) {
723        return mObjects.getValue(line, 0);
724    }
725
726    @Override
727    public int getTopPadding() {
728        return mTopPadding;
729    }
730
731    @Override
732    public int getBottomPadding() {
733        return mBottomPadding;
734    }
735
736    /**
737     * @hide
738     */
739    @Override
740    public int getHyphen(int line) {
741        return mInts.getValue(line, HYPHEN) & HYPHEN_MASK;
742    }
743
744    private boolean getContentMayProtrudeFromTopOrBottom(int line) {
745        return (mInts.getValue(line, MAY_PROTRUDE_FROM_TOP_OR_BOTTOM)
746                & MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK) != 0;
747    }
748
749    @Override
750    public int getEllipsizedWidth() {
751        return mEllipsizedWidth;
752    }
753
754    private static class ChangeWatcher implements TextWatcher, SpanWatcher {
755        public ChangeWatcher(DynamicLayout layout) {
756            mLayout = new WeakReference<DynamicLayout>(layout);
757        }
758
759        private void reflow(CharSequence s, int where, int before, int after) {
760            DynamicLayout ml = mLayout.get();
761
762            if (ml != null)
763                ml.reflow(s, where, before, after);
764            else if (s instanceof Spannable)
765                ((Spannable) s).removeSpan(this);
766        }
767
768        public void beforeTextChanged(CharSequence s, int where, int before, int after) {
769            // Intentionally empty
770        }
771
772        public void onTextChanged(CharSequence s, int where, int before, int after) {
773            reflow(s, where, before, after);
774        }
775
776        public void afterTextChanged(Editable s) {
777            // Intentionally empty
778        }
779
780        public void onSpanAdded(Spannable s, Object o, int start, int end) {
781            if (o instanceof UpdateLayout)
782                reflow(s, start, end - start, end - start);
783        }
784
785        public void onSpanRemoved(Spannable s, Object o, int start, int end) {
786            if (o instanceof UpdateLayout)
787                reflow(s, start, end - start, end - start);
788        }
789
790        public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) {
791            if (o instanceof UpdateLayout) {
792                reflow(s, start, end - start, end - start);
793                reflow(s, nstart, nend - nstart, nend - nstart);
794            }
795        }
796
797        private WeakReference<DynamicLayout> mLayout;
798    }
799
800    @Override
801    public int getEllipsisStart(int line) {
802        if (mEllipsizeAt == null) {
803            return 0;
804        }
805
806        return mInts.getValue(line, ELLIPSIS_START);
807    }
808
809    @Override
810    public int getEllipsisCount(int line) {
811        if (mEllipsizeAt == null) {
812            return 0;
813        }
814
815        return mInts.getValue(line, ELLIPSIS_COUNT);
816    }
817
818    private CharSequence mBase;
819    private CharSequence mDisplay;
820    private ChangeWatcher mWatcher;
821    private boolean mIncludePad;
822    private boolean mEllipsize;
823    private int mEllipsizedWidth;
824    private TextUtils.TruncateAt mEllipsizeAt;
825    private int mBreakStrategy;
826    private int mHyphenationFrequency;
827    private int mJustificationMode;
828
829    private PackedIntVector mInts;
830    private PackedObjectVector<Directions> mObjects;
831
832    /**
833     * Value used in mBlockIndices when a block has been created or recycled and indicating that its
834     * display list needs to be re-created.
835     * @hide
836     */
837    public static final int INVALID_BLOCK_INDEX = -1;
838    // Stores the line numbers of the last line of each block (inclusive)
839    private int[] mBlockEndLines;
840    // The indices of this block's display list in TextView's internal display list array or
841    // INVALID_BLOCK_INDEX if this block has been invalidated during an edition
842    private int[] mBlockIndices;
843    // Set of blocks that always need to be redrawn.
844    private ArraySet<Integer> mBlocksAlwaysNeedToBeRedrawn;
845    // Number of items actually currently being used in the above 2 arrays
846    private int mNumberOfBlocks;
847    // The first index of the blocks whose locations are changed
848    private int mIndexFirstChangedBlock;
849
850    private int mTopPadding, mBottomPadding;
851
852    private Rect mTempRect = new Rect();
853
854    private static StaticLayout sStaticLayout = null;
855    private static StaticLayout.Builder sBuilder = null;
856
857    private static final Object[] sLock = new Object[0];
858
859    // START, DIR, and TAB share the same entry.
860    private static final int START = 0;
861    private static final int DIR = START;
862    private static final int TAB = START;
863    private static final int TOP = 1;
864    private static final int DESCENT = 2;
865    private static final int EXTRA = 3;
866    // HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry.
867    private static final int HYPHEN = 4;
868    private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN;
869    private static final int COLUMNS_NORMAL = 5;
870
871    private static final int ELLIPSIS_START = 5;
872    private static final int ELLIPSIS_COUNT = 6;
873    private static final int COLUMNS_ELLIPSIZE = 7;
874
875    private static final int START_MASK = 0x1FFFFFFF;
876    private static final int DIR_SHIFT  = 30;
877    private static final int TAB_MASK   = 0x20000000;
878    private static final int HYPHEN_MASK = 0xFF;
879    private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK = 0x100;
880
881    private static final int ELLIPSIS_UNDEFINED = 0x80000000;
882}
883