DynamicLayout.java revision 1e130b2abc051081982b5a793a18a28376c945e4
1/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.text;
18
19import android.graphics.Paint;
20import android.text.style.UpdateLayout;
21import android.text.style.WrapTogetherSpan;
22
23import com.android.internal.util.ArrayUtils;
24
25import java.lang.ref.WeakReference;
26
27/**
28 * DynamicLayout is a text layout that updates itself as the text is edited.
29 * <p>This is used by widgets to control text layout. You should not need
30 * to use this class directly unless you are implementing your own widget
31 * or custom display object, or need to call
32 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
33 *  Canvas.drawText()} directly.</p>
34 */
35public class DynamicLayout extends Layout
36{
37    private static final int PRIORITY = 128;
38
39    /**
40     * Make a layout for the specified text that will be updated as
41     * the text is changed.
42     */
43    public DynamicLayout(CharSequence base,
44                         TextPaint paint,
45                         int width, Alignment align,
46                         float spacingmult, float spacingadd,
47                         boolean includepad) {
48        this(base, base, paint, width, align, spacingmult, spacingadd,
49             includepad);
50    }
51
52    /**
53     * Make a layout for the transformed text (password transformation
54     * being the primary example of a transformation)
55     * that will be updated as the base text is changed.
56     */
57    public DynamicLayout(CharSequence base, CharSequence display,
58                         TextPaint paint,
59                         int width, Alignment align,
60                         float spacingmult, float spacingadd,
61                         boolean includepad) {
62        this(base, display, paint, width, align, spacingmult, spacingadd,
63             includepad, null, 0);
64    }
65
66    /**
67     * Make a layout for the transformed text (password transformation
68     * being the primary example of a transformation)
69     * that will be updated as the base text is changed.
70     * If ellipsize is non-null, the Layout will ellipsize the text
71     * down to ellipsizedWidth.
72     */
73    public DynamicLayout(CharSequence base, CharSequence display,
74                         TextPaint paint,
75                         int width, Alignment align,
76                         float spacingmult, float spacingadd,
77                         boolean includepad,
78                         TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
79        this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
80                spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth);
81    }
82
83    /**
84     * Make a layout for the transformed text (password transformation
85     * being the primary example of a transformation)
86     * that will be updated as the base text is changed.
87     * If ellipsize is non-null, the Layout will ellipsize the text
88     * down to ellipsizedWidth.
89     * *
90     * *@hide
91     */
92    public DynamicLayout(CharSequence base, CharSequence display,
93                         TextPaint paint,
94                         int width, Alignment align, TextDirectionHeuristic textDir,
95                         float spacingmult, float spacingadd,
96                         boolean includepad,
97                         TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
98        super((ellipsize == null)
99                ? display
100                : (display instanceof Spanned)
101                    ? new SpannedEllipsizer(display)
102                    : new Ellipsizer(display),
103              paint, width, align, textDir, spacingmult, spacingadd);
104
105        mBase = base;
106        mDisplay = display;
107
108        if (ellipsize != null) {
109            mInts = new PackedIntVector(COLUMNS_ELLIPSIZE);
110            mEllipsizedWidth = ellipsizedWidth;
111            mEllipsizeAt = ellipsize;
112        } else {
113            mInts = new PackedIntVector(COLUMNS_NORMAL);
114            mEllipsizedWidth = width;
115            mEllipsizeAt = null;
116        }
117
118        mObjects = new PackedObjectVector<Directions>(1);
119
120        mBlockEnds = new int[] { 0 };
121        mBlockIndices = new int[] { INVALID_BLOCK_INDEX };
122        mNumberOfBlocks = 1;
123
124        mIncludePad = includepad;
125
126        /*
127         * This is annoying, but we can't refer to the layout until
128         * superclass construction is finished, and the superclass
129         * constructor wants the reference to the display text.
130         *
131         * This will break if the superclass constructor ever actually
132         * cares about the content instead of just holding the reference.
133         */
134        if (ellipsize != null) {
135            Ellipsizer e = (Ellipsizer) getText();
136
137            e.mLayout = this;
138            e.mWidth = ellipsizedWidth;
139            e.mMethod = ellipsize;
140            mEllipsize = true;
141        }
142
143        // Initial state is a single line with 0 characters (0 to 0),
144        // with top at 0 and bottom at whatever is natural, and
145        // undefined ellipsis.
146
147        int[] start;
148
149        if (ellipsize != null) {
150            start = new int[COLUMNS_ELLIPSIZE];
151            start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
152        } else {
153            start = new int[COLUMNS_NORMAL];
154        }
155
156        Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT };
157
158        Paint.FontMetricsInt fm = paint.getFontMetricsInt();
159        int asc = fm.ascent;
160        int desc = fm.descent;
161
162        start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT;
163        start[TOP] = 0;
164        start[DESCENT] = desc;
165        mInts.insertAt(0, start);
166
167        start[TOP] = desc - asc;
168        mInts.insertAt(1, start);
169
170        mObjects.insertAt(0, dirs);
171
172        // Update from 0 characters to whatever the real text is
173
174        reflow(base, 0, 0, base.length());
175
176        if (base instanceof Spannable) {
177            if (mWatcher == null)
178                mWatcher = new ChangeWatcher(this);
179
180            // Strip out any watchers for other DynamicLayouts.
181            Spannable sp = (Spannable) base;
182            ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class);
183            for (int i = 0; i < spans.length; i++)
184                sp.removeSpan(spans[i]);
185
186            sp.setSpan(mWatcher, 0, base.length(),
187                       Spannable.SPAN_INCLUSIVE_INCLUSIVE |
188                       (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT));
189        }
190    }
191
192    private void reflow(CharSequence s, int where, int before, int after) {
193        if (s != mBase)
194            return;
195
196        CharSequence text = mDisplay;
197        int len = text.length();
198
199        // seek back to the start of the paragraph
200
201        int find = TextUtils.lastIndexOf(text, '\n', where - 1);
202        if (find < 0)
203            find = 0;
204        else
205            find = find + 1;
206
207        {
208            int diff = where - find;
209            before += diff;
210            after += diff;
211            where -= diff;
212        }
213
214        // seek forward to the end of the paragraph
215
216        int look = TextUtils.indexOf(text, '\n', where + after);
217        if (look < 0)
218            look = len;
219        else
220            look++; // we want the index after the \n
221
222        int change = look - (where + after);
223        before += change;
224        after += change;
225
226        // seek further out to cover anything that is forced to wrap together
227
228        if (text instanceof Spanned) {
229            Spanned sp = (Spanned) text;
230            boolean again;
231
232            do {
233                again = false;
234
235                Object[] force = sp.getSpans(where, where + after,
236                                             WrapTogetherSpan.class);
237
238                for (int i = 0; i < force.length; i++) {
239                    int st = sp.getSpanStart(force[i]);
240                    int en = sp.getSpanEnd(force[i]);
241
242                    if (st < where) {
243                        again = true;
244
245                        int diff = where - st;
246                        before += diff;
247                        after += diff;
248                        where -= diff;
249                    }
250
251                    if (en > where + after) {
252                        again = true;
253
254                        int diff = en - (where + after);
255                        before += diff;
256                        after += diff;
257                    }
258                }
259            } while (again);
260        }
261
262        // find affected region of old layout
263
264        int startline = getLineForOffset(where);
265        int startv = getLineTop(startline);
266
267        int endline = getLineForOffset(where + before);
268        if (where + after == len)
269            endline = getLineCount();
270        int endv = getLineTop(endline);
271        boolean islast = (endline == getLineCount());
272
273        // generate new layout for affected text
274
275        StaticLayout reflowed;
276
277        synchronized (sLock) {
278            reflowed = sStaticLayout;
279            sStaticLayout = null;
280        }
281
282        if (reflowed == null) {
283            reflowed = new StaticLayout(null);
284        } else {
285            reflowed.prepare();
286        }
287
288        reflowed.generate(text, where, where + after,
289                getPaint(), getWidth(), getTextDirectionHeuristic(), getSpacingMultiplier(),
290                getSpacingAdd(), false,
291                true, mEllipsizedWidth, mEllipsizeAt);
292        int n = reflowed.getLineCount();
293
294        // If the new layout has a blank line at the end, but it is not
295        // the very end of the buffer, then we already have a line that
296        // starts there, so disregard the blank line.
297
298        if (where + after != len &&
299            reflowed.getLineStart(n - 1) == where + after)
300            n--;
301
302        // remove affected lines from old layout
303        mInts.deleteAt(startline, endline - startline);
304        mObjects.deleteAt(startline, endline - startline);
305        updateBlocks(startline, endline - 1, n);
306
307        // adjust offsets in layout for new height and offsets
308
309        int ht = reflowed.getLineTop(n);
310        int toppad = 0, botpad = 0;
311
312        if (mIncludePad && startline == 0) {
313            toppad = reflowed.getTopPadding();
314            mTopPadding = toppad;
315            ht -= toppad;
316        }
317        if (mIncludePad && islast) {
318            botpad = reflowed.getBottomPadding();
319            mBottomPadding = botpad;
320            ht += botpad;
321        }
322
323        mInts.adjustValuesBelow(startline, START, after - before);
324        mInts.adjustValuesBelow(startline, TOP, startv - endv + ht);
325
326        // insert new layout
327
328        int[] ints;
329
330        if (mEllipsize) {
331            ints = new int[COLUMNS_ELLIPSIZE];
332            ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
333        } else {
334            ints = new int[COLUMNS_NORMAL];
335        }
336
337        Directions[] objects = new Directions[1];
338
339        for (int i = 0; i < n; i++) {
340            ints[START] = reflowed.getLineStart(i) |
341                          (reflowed.getParagraphDirection(i) << DIR_SHIFT) |
342                          (reflowed.getLineContainsTab(i) ? TAB_MASK : 0);
343
344            int top = reflowed.getLineTop(i) + startv;
345            if (i > 0)
346                top -= toppad;
347            ints[TOP] = top;
348
349            int desc = reflowed.getLineDescent(i);
350            if (i == n - 1)
351                desc += botpad;
352
353            ints[DESCENT] = desc;
354            objects[0] = reflowed.getLineDirections(i);
355
356            if (mEllipsize) {
357                ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
358                ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i);
359            }
360
361            mInts.insertAt(startline + i, ints);
362            mObjects.insertAt(startline + i, objects);
363        }
364
365        synchronized (sLock) {
366            sStaticLayout = reflowed;
367            reflowed.finish();
368        }
369    }
370
371    /**
372     * This method is called every time the layout is reflowed after an edition.
373     * It updates the internal block data structure. The text is split in blocks
374     * of contiguous lines, with at least one block for the entire text.
375     * When a range of lines is edited, new blocks (from 0 to 3 depending on the
376     * overlap structure) will replace the set of overlapping blocks.
377     * Blocks are listed in order and are represented by their ending line number.
378     * An index is associated to each block (which will be used by display lists),
379     * this class simply invalidates the index of blocks overlapping a modification.
380     *
381     * This method is package private and not private so that it can be tested.
382     *
383     * @param startLine the first line of the range of modified lines
384     * @param endLine the last line of the range, possibly equal to startLine, lower
385     * than getLineCount()
386     * @param newLineCount the number of lines that will replace the range, possibly 0
387     *
388     * @hide
389     */
390    void updateBlocks(int startLine, int endLine, int newLineCount) {
391        int firstBlock = -1;
392        int lastBlock = -1;
393        for (int i = 0; i < mNumberOfBlocks; i++) {
394            if (mBlockEnds[i] >= startLine) {
395                firstBlock = i;
396                break;
397            }
398        }
399        for (int i = firstBlock; i < mNumberOfBlocks; i++) {
400            if (mBlockEnds[i] >= endLine) {
401                lastBlock = i;
402                break;
403            }
404        }
405        final int lastBlockEndLine = mBlockEnds[lastBlock];
406
407        boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
408                mBlockEnds[firstBlock - 1] + 1);
409        boolean createBlock = newLineCount > 0;
410        boolean createBlockAfter = endLine < mBlockEnds[lastBlock];
411
412        int numAddedBlocks = 0;
413        if (createBlockBefore) numAddedBlocks++;
414        if (createBlock) numAddedBlocks++;
415        if (createBlockAfter) numAddedBlocks++;
416
417        final int numRemovedBlocks = lastBlock - firstBlock + 1;
418        final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks;
419
420        if (newNumberOfBlocks == 0) {
421            // Even when text is empty, there is actually one line and hence one block
422            mBlockEnds[0] = 0;
423            mBlockIndices[0] = INVALID_BLOCK_INDEX;
424            mNumberOfBlocks = 1;
425            return;
426        }
427
428        if (newNumberOfBlocks > mBlockEnds.length) {
429            final int newSize = ArrayUtils.idealIntArraySize(newNumberOfBlocks);
430            int[] blockEnds = new int[newSize];
431            int[] blockIndices = new int[newSize];
432            System.arraycopy(mBlockEnds, 0, blockEnds, 0, firstBlock);
433            System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock);
434            System.arraycopy(mBlockEnds, lastBlock + 1,
435                    blockEnds, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
436            System.arraycopy(mBlockIndices, lastBlock + 1,
437                    blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
438            mBlockEnds = blockEnds;
439            mBlockIndices = blockIndices;
440        } else {
441            System.arraycopy(mBlockEnds, lastBlock + 1,
442                    mBlockEnds, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
443            System.arraycopy(mBlockIndices, lastBlock + 1,
444                    mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
445        }
446
447        mNumberOfBlocks = newNumberOfBlocks;
448        final int deltaLines = newLineCount - (endLine - startLine + 1);
449        for (int i = firstBlock + numAddedBlocks; i < mNumberOfBlocks; i++) {
450            mBlockEnds[i] += deltaLines;
451        }
452
453        int blockIndex = firstBlock;
454        if (createBlockBefore) {
455            mBlockEnds[blockIndex] = startLine - 1;
456            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
457            blockIndex++;
458        }
459
460        if (createBlock) {
461            mBlockEnds[blockIndex] = startLine + newLineCount - 1;
462            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
463            blockIndex++;
464        }
465
466        if (createBlockAfter) {
467            mBlockEnds[blockIndex] = lastBlockEndLine + deltaLines;
468            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
469        }
470    }
471
472    /**
473     * This package private method is used for test purposes only
474     * @hide
475     */
476    void setBlocksDataForTest(int[] blockEnds, int[] blockIndices, int numberOfBlocks) {
477        mBlockEnds = new int[blockEnds.length];
478        mBlockIndices = new int[blockIndices.length];
479        System.arraycopy(blockEnds, 0, mBlockEnds, 0, blockEnds.length);
480        System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length);
481        mNumberOfBlocks = numberOfBlocks;
482    }
483
484    /**
485     * @hide
486     */
487    public int[] getBlockEnds() {
488        return mBlockEnds;
489    }
490
491    /**
492     * @hide
493     */
494    public int[] getBlockIndices() {
495        return mBlockIndices;
496    }
497
498    /**
499     * @hide
500     */
501    public int getNumberOfBlocks() {
502        return mNumberOfBlocks;
503    }
504
505    @Override
506    public int getLineCount() {
507        return mInts.size() - 1;
508    }
509
510    @Override
511    public int getLineTop(int line) {
512        return mInts.getValue(line, TOP);
513    }
514
515    @Override
516    public int getLineDescent(int line) {
517        return mInts.getValue(line, DESCENT);
518    }
519
520    @Override
521    public int getLineStart(int line) {
522        return mInts.getValue(line, START) & START_MASK;
523    }
524
525    @Override
526    public boolean getLineContainsTab(int line) {
527        return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
528    }
529
530    @Override
531    public int getParagraphDirection(int line) {
532        return mInts.getValue(line, DIR) >> DIR_SHIFT;
533    }
534
535    @Override
536    public final Directions getLineDirections(int line) {
537        return mObjects.getValue(line, 0);
538    }
539
540    @Override
541    public int getTopPadding() {
542        return mTopPadding;
543    }
544
545    @Override
546    public int getBottomPadding() {
547        return mBottomPadding;
548    }
549
550    @Override
551    public int getEllipsizedWidth() {
552        return mEllipsizedWidth;
553    }
554
555    private static class ChangeWatcher implements TextWatcher, SpanWatcher {
556        public ChangeWatcher(DynamicLayout layout) {
557            mLayout = new WeakReference<DynamicLayout>(layout);
558        }
559
560        private void reflow(CharSequence s, int where, int before, int after) {
561            DynamicLayout ml = mLayout.get();
562
563            if (ml != null)
564                ml.reflow(s, where, before, after);
565            else if (s instanceof Spannable)
566                ((Spannable) s).removeSpan(this);
567        }
568
569        public void beforeTextChanged(CharSequence s, int where, int before, int after) {
570            // Intentionally empty
571        }
572
573        public void onTextChanged(CharSequence s, int where, int before, int after) {
574            reflow(s, where, before, after);
575        }
576
577        public void afterTextChanged(Editable s) {
578            // Intentionally empty
579        }
580
581        public void onSpanAdded(Spannable s, Object o, int start, int end) {
582            if (o instanceof UpdateLayout)
583                reflow(s, start, end - start, end - start);
584        }
585
586        public void onSpanRemoved(Spannable s, Object o, int start, int end) {
587            if (o instanceof UpdateLayout)
588                reflow(s, start, end - start, end - start);
589        }
590
591        public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) {
592            if (o instanceof UpdateLayout) {
593                reflow(s, start, end - start, end - start);
594                reflow(s, nstart, nend - nstart, nend - nstart);
595            }
596        }
597
598        private WeakReference<DynamicLayout> mLayout;
599    }
600
601    @Override
602    public int getEllipsisStart(int line) {
603        if (mEllipsizeAt == null) {
604            return 0;
605        }
606
607        return mInts.getValue(line, ELLIPSIS_START);
608    }
609
610    @Override
611    public int getEllipsisCount(int line) {
612        if (mEllipsizeAt == null) {
613            return 0;
614        }
615
616        return mInts.getValue(line, ELLIPSIS_COUNT);
617    }
618
619    private CharSequence mBase;
620    private CharSequence mDisplay;
621    private ChangeWatcher mWatcher;
622    private boolean mIncludePad;
623    private boolean mEllipsize;
624    private int mEllipsizedWidth;
625    private TextUtils.TruncateAt mEllipsizeAt;
626
627    private PackedIntVector mInts;
628    private PackedObjectVector<Directions> mObjects;
629
630    /**
631     * Value used in mBlockIndices when a block has been created or recycled and indicating that its
632     * display list needs to be re-created.
633     * @hide
634     */
635    public static final int INVALID_BLOCK_INDEX = -1;
636    // Stores the line numbers of the last line of each block
637    private int[] mBlockEnds;
638    // The indices of this block's display list in TextView's internal display list array or
639    // INVALID_BLOCK_INDEX if this block has been invalidated during an edition
640    private int[] mBlockIndices;
641    // Number of items actually currently being used in the above 2 arrays
642    private int mNumberOfBlocks;
643
644    private int mTopPadding, mBottomPadding;
645
646    private static StaticLayout sStaticLayout = new StaticLayout(null);
647
648    private static final Object[] sLock = new Object[0];
649
650    private static final int START = 0;
651    private static final int DIR = START;
652    private static final int TAB = START;
653    private static final int TOP = 1;
654    private static final int DESCENT = 2;
655    private static final int COLUMNS_NORMAL = 3;
656
657    private static final int ELLIPSIS_START = 3;
658    private static final int ELLIPSIS_COUNT = 4;
659    private static final int COLUMNS_ELLIPSIZE = 5;
660
661    private static final int START_MASK = 0x1FFFFFFF;
662    private static final int DIR_SHIFT  = 30;
663    private static final int TAB_MASK   = 0x20000000;
664
665    private static final int ELLIPSIS_UNDEFINED = 0x80000000;
666}
667