DynamicLayout.java revision fc77869ee06c05eb5b7deea74e5ad31bd89a85bd
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     * @param startLine the first line of the range of modified lines
495     * @param endLine the last line of the range, possibly equal to startLine, lower
496     * than getLineCount()
497     * @param newLineCount the number of lines that will replace the range, possibly 0
498     *
499     * @hide
500     */
501    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
502    public void updateBlocks(int startLine, int endLine, int newLineCount) {
503        if (mBlockEndLines == null) {
504            createBlocks();
505            return;
506        }
507
508        int firstBlock = -1;
509        int lastBlock = -1;
510        for (int i = 0; i < mNumberOfBlocks; i++) {
511            if (mBlockEndLines[i] >= startLine) {
512                firstBlock = i;
513                break;
514            }
515        }
516        for (int i = firstBlock; i < mNumberOfBlocks; i++) {
517            if (mBlockEndLines[i] >= endLine) {
518                lastBlock = i;
519                break;
520            }
521        }
522        final int lastBlockEndLine = mBlockEndLines[lastBlock];
523
524        boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
525                mBlockEndLines[firstBlock - 1] + 1);
526        boolean createBlock = newLineCount > 0;
527        boolean createBlockAfter = endLine < mBlockEndLines[lastBlock];
528
529        int numAddedBlocks = 0;
530        if (createBlockBefore) numAddedBlocks++;
531        if (createBlock) numAddedBlocks++;
532        if (createBlockAfter) numAddedBlocks++;
533
534        final int numRemovedBlocks = lastBlock - firstBlock + 1;
535        final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks;
536
537        if (newNumberOfBlocks == 0) {
538            // Even when text is empty, there is actually one line and hence one block
539            mBlockEndLines[0] = 0;
540            mBlockIndices[0] = INVALID_BLOCK_INDEX;
541            mNumberOfBlocks = 1;
542            return;
543        }
544
545        if (newNumberOfBlocks > mBlockEndLines.length) {
546            int[] blockEndLines = ArrayUtils.newUnpaddedIntArray(
547                    Math.max(mBlockEndLines.length * 2, newNumberOfBlocks));
548            int[] blockIndices = new int[blockEndLines.length];
549            System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock);
550            System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock);
551            System.arraycopy(mBlockEndLines, lastBlock + 1,
552                    blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
553            System.arraycopy(mBlockIndices, lastBlock + 1,
554                    blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
555            mBlockEndLines = blockEndLines;
556            mBlockIndices = blockIndices;
557        } else if (numAddedBlocks + numRemovedBlocks != 0) {
558            System.arraycopy(mBlockEndLines, lastBlock + 1,
559                    mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
560            System.arraycopy(mBlockIndices, lastBlock + 1,
561                    mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
562        }
563
564        if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) {
565            final ArraySet<Integer> set = new ArraySet<>();
566            for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) {
567                Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i);
568                if (block > firstBlock) {
569                    block += numAddedBlocks - numRemovedBlocks;
570                }
571                set.add(block);
572            }
573            mBlocksAlwaysNeedToBeRedrawn = set;
574        }
575
576        mNumberOfBlocks = newNumberOfBlocks;
577        int newFirstChangedBlock;
578        final int deltaLines = newLineCount - (endLine - startLine + 1);
579        if (deltaLines != 0) {
580            // Display list whose index is >= mIndexFirstChangedBlock is valid
581            // but it needs to update its drawing location.
582            newFirstChangedBlock = firstBlock + numAddedBlocks;
583            for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) {
584                mBlockEndLines[i] += deltaLines;
585            }
586        } else {
587            newFirstChangedBlock = mNumberOfBlocks;
588        }
589        mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock);
590
591        int blockIndex = firstBlock;
592        if (createBlockBefore) {
593            mBlockEndLines[blockIndex] = startLine - 1;
594            updateAlwaysNeedsToBeRedrawn(blockIndex);
595            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
596            blockIndex++;
597        }
598
599        if (createBlock) {
600            mBlockEndLines[blockIndex] = startLine + newLineCount - 1;
601            updateAlwaysNeedsToBeRedrawn(blockIndex);
602            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
603            blockIndex++;
604        }
605
606        if (createBlockAfter) {
607            mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines;
608            updateAlwaysNeedsToBeRedrawn(blockIndex);
609            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
610        }
611    }
612
613    /**
614     * This method is used for test purposes only.
615     * @hide
616     */
617    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
618    public void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks,
619            int totalLines) {
620        mBlockEndLines = new int[blockEndLines.length];
621        mBlockIndices = new int[blockIndices.length];
622        System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length);
623        System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length);
624        mNumberOfBlocks = numberOfBlocks;
625        while (mInts.size() < totalLines) {
626            mInts.insertAt(mInts.size(), new int[COLUMNS_NORMAL]);
627        }
628    }
629
630    /**
631     * @hide
632     */
633    public int[] getBlockEndLines() {
634        return mBlockEndLines;
635    }
636
637    /**
638     * @hide
639     */
640    public int[] getBlockIndices() {
641        return mBlockIndices;
642    }
643
644    /**
645     * @hide
646     */
647    public int getBlockIndex(int index) {
648        return mBlockIndices[index];
649    }
650
651    /**
652     * @hide
653     * @param index
654     */
655    public void setBlockIndex(int index, int blockIndex) {
656        mBlockIndices[index] = blockIndex;
657    }
658
659    /**
660     * @hide
661     */
662    public int getNumberOfBlocks() {
663        return mNumberOfBlocks;
664    }
665
666    /**
667     * @hide
668     */
669    public int getIndexFirstChangedBlock() {
670        return mIndexFirstChangedBlock;
671    }
672
673    /**
674     * @hide
675     */
676    public void setIndexFirstChangedBlock(int i) {
677        mIndexFirstChangedBlock = i;
678    }
679
680    @Override
681    public int getLineCount() {
682        return mInts.size() - 1;
683    }
684
685    @Override
686    public int getLineTop(int line) {
687        return mInts.getValue(line, TOP);
688    }
689
690    @Override
691    public int getLineDescent(int line) {
692        return mInts.getValue(line, DESCENT);
693    }
694
695    @Override
696    public int getLineStart(int line) {
697        return mInts.getValue(line, START) & START_MASK;
698    }
699
700    @Override
701    public boolean getLineContainsTab(int line) {
702        return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
703    }
704
705    @Override
706    public int getParagraphDirection(int line) {
707        return mInts.getValue(line, DIR) >> DIR_SHIFT;
708    }
709
710    @Override
711    public final Directions getLineDirections(int line) {
712        return mObjects.getValue(line, 0);
713    }
714
715    @Override
716    public int getTopPadding() {
717        return mTopPadding;
718    }
719
720    @Override
721    public int getBottomPadding() {
722        return mBottomPadding;
723    }
724
725    /**
726     * @hide
727     */
728    @Override
729    public int getHyphen(int line) {
730        return mInts.getValue(line, HYPHEN) & HYPHEN_MASK;
731    }
732
733    private boolean getContentMayProtrudeFromTopOrBottom(int line) {
734        return (mInts.getValue(line, MAY_PROTRUDE_FROM_TOP_OR_BOTTOM)
735                & MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK) != 0;
736    }
737
738    @Override
739    public int getEllipsizedWidth() {
740        return mEllipsizedWidth;
741    }
742
743    private static class ChangeWatcher implements TextWatcher, SpanWatcher {
744        public ChangeWatcher(DynamicLayout layout) {
745            mLayout = new WeakReference<DynamicLayout>(layout);
746        }
747
748        private void reflow(CharSequence s, int where, int before, int after) {
749            DynamicLayout ml = mLayout.get();
750
751            if (ml != null)
752                ml.reflow(s, where, before, after);
753            else if (s instanceof Spannable)
754                ((Spannable) s).removeSpan(this);
755        }
756
757        public void beforeTextChanged(CharSequence s, int where, int before, int after) {
758            // Intentionally empty
759        }
760
761        public void onTextChanged(CharSequence s, int where, int before, int after) {
762            reflow(s, where, before, after);
763        }
764
765        public void afterTextChanged(Editable s) {
766            // Intentionally empty
767        }
768
769        public void onSpanAdded(Spannable s, Object o, int start, int end) {
770            if (o instanceof UpdateLayout)
771                reflow(s, start, end - start, end - start);
772        }
773
774        public void onSpanRemoved(Spannable s, Object o, int start, int end) {
775            if (o instanceof UpdateLayout)
776                reflow(s, start, end - start, end - start);
777        }
778
779        public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) {
780            if (o instanceof UpdateLayout) {
781                reflow(s, start, end - start, end - start);
782                reflow(s, nstart, nend - nstart, nend - nstart);
783            }
784        }
785
786        private WeakReference<DynamicLayout> mLayout;
787    }
788
789    @Override
790    public int getEllipsisStart(int line) {
791        if (mEllipsizeAt == null) {
792            return 0;
793        }
794
795        return mInts.getValue(line, ELLIPSIS_START);
796    }
797
798    @Override
799    public int getEllipsisCount(int line) {
800        if (mEllipsizeAt == null) {
801            return 0;
802        }
803
804        return mInts.getValue(line, ELLIPSIS_COUNT);
805    }
806
807    private CharSequence mBase;
808    private CharSequence mDisplay;
809    private ChangeWatcher mWatcher;
810    private boolean mIncludePad;
811    private boolean mEllipsize;
812    private int mEllipsizedWidth;
813    private TextUtils.TruncateAt mEllipsizeAt;
814    private int mBreakStrategy;
815    private int mHyphenationFrequency;
816    private int mJustificationMode;
817
818    private PackedIntVector mInts;
819    private PackedObjectVector<Directions> mObjects;
820
821    /**
822     * Value used in mBlockIndices when a block has been created or recycled and indicating that its
823     * display list needs to be re-created.
824     * @hide
825     */
826    public static final int INVALID_BLOCK_INDEX = -1;
827    // Stores the line numbers of the last line of each block (inclusive)
828    private int[] mBlockEndLines;
829    // The indices of this block's display list in TextView's internal display list array or
830    // INVALID_BLOCK_INDEX if this block has been invalidated during an edition
831    private int[] mBlockIndices;
832    // Set of blocks that always need to be redrawn.
833    private ArraySet<Integer> mBlocksAlwaysNeedToBeRedrawn;
834    // Number of items actually currently being used in the above 2 arrays
835    private int mNumberOfBlocks;
836    // The first index of the blocks whose locations are changed
837    private int mIndexFirstChangedBlock;
838
839    private int mTopPadding, mBottomPadding;
840
841    private Rect mTempRect = new Rect();
842
843    private static StaticLayout sStaticLayout = null;
844    private static StaticLayout.Builder sBuilder = null;
845
846    private static final Object[] sLock = new Object[0];
847
848    // START, DIR, and TAB share the same entry.
849    private static final int START = 0;
850    private static final int DIR = START;
851    private static final int TAB = START;
852    private static final int TOP = 1;
853    private static final int DESCENT = 2;
854    // HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry.
855    private static final int HYPHEN = 3;
856    private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN;
857    private static final int COLUMNS_NORMAL = 4;
858
859    private static final int ELLIPSIS_START = 4;
860    private static final int ELLIPSIS_COUNT = 5;
861    private static final int COLUMNS_ELLIPSIZE = 6;
862
863    private static final int START_MASK = 0x1FFFFFFF;
864    private static final int DIR_SHIFT  = 30;
865    private static final int TAB_MASK   = 0x20000000;
866    private static final int HYPHEN_MASK = 0xFF;
867    private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK = 0x100;
868
869    private static final int ELLIPSIS_UNDEFINED = 0x80000000;
870}
871