DynamicLayout.java revision a130e5f59dc6b2117e4c1a8ffef54828e9ea44c7
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 java.lang.ref.WeakReference;
24
25/**
26 * DynamicLayout is a text layout that updates itself as the text is edited.
27 * <p>This is used by widgets to control text layout. You should not need
28 * to use this class directly unless you are implementing your own widget
29 * or custom display object, or need to call
30 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
31 *  Canvas.drawText()} directly.</p>
32 */
33public class DynamicLayout
34extends Layout
35{
36    private static final int PRIORITY = 128;
37
38    /**
39     * Make a layout for the specified text that will be updated as
40     * the text is changed.
41     */
42    public DynamicLayout(CharSequence base,
43                         TextPaint paint,
44                         int width, Alignment align,
45                         float spacingmult, float spacingadd,
46                         boolean includepad) {
47        this(base, base, paint, width, align, spacingmult, spacingadd,
48             includepad);
49    }
50
51    /**
52     * Make a layout for the transformed text (password transformation
53     * being the primary example of a transformation)
54     * that will be updated as the base text is changed.
55     */
56    public DynamicLayout(CharSequence base, CharSequence display,
57                         TextPaint paint,
58                         int width, Alignment align,
59                         float spacingmult, float spacingadd,
60                         boolean includepad) {
61        this(base, display, paint, width, align, spacingmult, spacingadd,
62             includepad, null, 0);
63    }
64
65    /**
66     * Make a layout for the transformed text (password transformation
67     * being the primary example of a transformation)
68     * that will be updated as the base text is changed.
69     * If ellipsize is non-null, the Layout will ellipsize the text
70     * down to ellipsizedWidth.
71     */
72    public DynamicLayout(CharSequence base, CharSequence display,
73                         TextPaint paint,
74                         int width, Alignment align,
75                         float spacingmult, float spacingadd,
76                         boolean includepad,
77                         TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
78        this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
79                spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
80    }
81
82    /**
83     * Make a layout for the transformed text (password transformation
84     * being the primary example of a transformation)
85     * that will be updated as the base text is changed.
86     * If ellipsize is non-null, the Layout will ellipsize the text
87     * down to ellipsizedWidth.
88     * *
89     * *@hide
90     */
91    public DynamicLayout(CharSequence base, CharSequence display,
92                         TextPaint paint,
93                         int width, Alignment align, TextDirectionHeuristic textDir,
94                         float spacingmult, float spacingadd,
95                         boolean includepad,
96                         TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) {
97        super((ellipsize == null)
98                ? display
99                : (display instanceof Spanned)
100                    ? new SpannedEllipsizer(display)
101                    : new Ellipsizer(display),
102              paint, width, align, textDir, spacingmult, spacingadd);
103
104        mBase = base;
105        mDisplay = display;
106
107        if (ellipsize != null) {
108            mInts = new PackedIntVector(COLUMNS_ELLIPSIZE);
109            mEllipsizedWidth = ellipsizedWidth;
110            mEllipsizeAt = ellipsize;
111        } else {
112            mInts = new PackedIntVector(COLUMNS_NORMAL);
113            mEllipsizedWidth = width;
114            mEllipsizeAt = null;
115        }
116
117        mObjects = new PackedObjectVector<Directions>(1);
118
119        mIncludePad = includepad;
120
121        /*
122         * This is annoying, but we can't refer to the layout until
123         * superclass construction is finished, and the superclass
124         * constructor wants the reference to the display text.
125         *
126         * This will break if the superclass constructor ever actually
127         * cares about the content instead of just holding the reference.
128         */
129        if (ellipsize != null) {
130            Ellipsizer e = (Ellipsizer) getText();
131
132            e.mLayout = this;
133            e.mWidth = ellipsizedWidth;
134            e.mMethod = ellipsize;
135            mEllipsize = true;
136        }
137
138        mMaxLines = maxLines;
139
140        // Initial state is a single line with 0 characters (0 to 0),
141        // with top at 0 and bottom at whatever is natural, and
142        // undefined ellipsis.
143
144        int[] start;
145
146        if (ellipsize != null) {
147            start = new int[COLUMNS_ELLIPSIZE];
148            start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
149        } else {
150            start = new int[COLUMNS_NORMAL];
151        }
152
153        Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT };
154
155        Paint.FontMetricsInt fm = paint.getFontMetricsInt();
156        int asc = fm.ascent;
157        int desc = fm.descent;
158
159        start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT;
160        start[TOP] = 0;
161        start[DESCENT] = desc;
162        mInts.insertAt(0, start);
163
164        start[TOP] = desc - asc;
165        mInts.insertAt(1, start);
166
167        mObjects.insertAt(0, dirs);
168
169        // Update from 0 characters to whatever the real text is
170
171        reflow(base, 0, 0, base.length());
172
173        if (base instanceof Spannable) {
174            if (mWatcher == null)
175                mWatcher = new ChangeWatcher(this);
176
177            // Strip out any watchers for other DynamicLayouts.
178            Spannable sp = (Spannable) base;
179            ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class);
180            for (int i = 0; i < spans.length; i++)
181                sp.removeSpan(spans[i]);
182
183            sp.setSpan(mWatcher, 0, base.length(),
184                       Spannable.SPAN_INCLUSIVE_INCLUSIVE |
185                       (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT));
186        }
187    }
188
189    private void reflow(CharSequence s, int where, int before, int after) {
190        if (s != mBase)
191            return;
192
193        CharSequence text = mDisplay;
194        int len = text.length();
195
196        // seek back to the start of the paragraph
197
198        int find = TextUtils.lastIndexOf(text, '\n', where - 1);
199        if (find < 0)
200            find = 0;
201        else
202            find = find + 1;
203
204        {
205            int diff = where - find;
206            before += diff;
207            after += diff;
208            where -= diff;
209        }
210
211        // seek forward to the end of the paragraph
212
213        int look = TextUtils.indexOf(text, '\n', where + after);
214        if (look < 0)
215            look = len;
216        else
217            look++; // we want the index after the \n
218
219        int change = look - (where + after);
220        before += change;
221        after += change;
222
223        // seek further out to cover anything that is forced to wrap together
224
225        if (text instanceof Spanned) {
226            Spanned sp = (Spanned) text;
227            boolean again;
228
229            do {
230                again = false;
231
232                Object[] force = sp.getSpans(where, where + after,
233                                             WrapTogetherSpan.class);
234
235                for (int i = 0; i < force.length; i++) {
236                    int st = sp.getSpanStart(force[i]);
237                    int en = sp.getSpanEnd(force[i]);
238
239                    if (st < where) {
240                        again = true;
241
242                        int diff = where - st;
243                        before += diff;
244                        after += diff;
245                        where -= diff;
246                    }
247
248                    if (en > where + after) {
249                        again = true;
250
251                        int diff = en - (where + after);
252                        before += diff;
253                        after += diff;
254                    }
255                }
256            } while (again);
257        }
258
259        // find affected region of old layout
260
261        int startline = getLineForOffset(where);
262        int startv = getLineTop(startline);
263
264        int endline = getLineForOffset(where + before);
265        if (where + after == len)
266            endline = getLineCount();
267        int endv = getLineTop(endline);
268        boolean islast = (endline == getLineCount());
269
270        // generate new layout for affected text
271
272        StaticLayout reflowed;
273
274        synchronized (sLock) {
275            reflowed = sStaticLayout;
276            sStaticLayout = null;
277        }
278
279        if (reflowed == null) {
280            reflowed = new StaticLayout(null);
281        } else {
282            reflowed.prepare();
283        }
284
285        reflowed.generate(text, where, where + after,
286                getPaint(), getWidth(), getAlignment(), getTextDirectionHeuristic(),
287                getSpacingMultiplier(), getSpacingAdd(),
288                false, true, mEllipsizedWidth, mEllipsizeAt, mMaxLines);
289        int n = reflowed.getLineCount();
290
291        // If the new layout has a blank line at the end, but it is not
292        // the very end of the buffer, then we already have a line that
293        // starts there, so disregard the blank line.
294
295        if (where + after != len &&
296            reflowed.getLineStart(n - 1) == where + after)
297            n--;
298
299        // remove affected lines from old layout
300
301        mInts.deleteAt(startline, endline - startline);
302        mObjects.deleteAt(startline, endline - startline);
303
304        // adjust offsets in layout for new height and offsets
305
306        int ht = reflowed.getLineTop(n);
307        int toppad = 0, botpad = 0;
308
309        if (mIncludePad && startline == 0) {
310            toppad = reflowed.getTopPadding();
311            mTopPadding = toppad;
312            ht -= toppad;
313        }
314        if (mIncludePad && islast) {
315            botpad = reflowed.getBottomPadding();
316            mBottomPadding = botpad;
317            ht += botpad;
318        }
319
320        mInts.adjustValuesBelow(startline, START, after - before);
321        mInts.adjustValuesBelow(startline, TOP, startv - endv + ht);
322
323        // insert new layout
324
325        int[] ints;
326
327        if (mEllipsize) {
328            ints = new int[COLUMNS_ELLIPSIZE];
329            ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
330        } else {
331            ints = new int[COLUMNS_NORMAL];
332        }
333
334        Directions[] objects = new Directions[1];
335
336        for (int i = 0; i < n; i++) {
337            ints[START] = reflowed.getLineStart(i) |
338                          (reflowed.getParagraphDirection(i) << DIR_SHIFT) |
339                          (reflowed.getLineContainsTab(i) ? TAB_MASK : 0);
340
341            int top = reflowed.getLineTop(i) + startv;
342            if (i > 0)
343                top -= toppad;
344            ints[TOP] = top;
345
346            int desc = reflowed.getLineDescent(i);
347            if (i == n - 1)
348                desc += botpad;
349
350            ints[DESCENT] = desc;
351            objects[0] = reflowed.getLineDirections(i);
352
353            if (mEllipsize) {
354                ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
355                ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i);
356            }
357
358            mInts.insertAt(startline + i, ints);
359            mObjects.insertAt(startline + i, objects);
360        }
361
362        synchronized (sLock) {
363            sStaticLayout = reflowed;
364            reflowed.finish();
365        }
366    }
367
368    @Override
369    public int getLineCount() {
370        return mInts.size() - 1;
371    }
372
373    @Override
374    public int getLineTop(int line) {
375        return mInts.getValue(line, TOP);
376    }
377
378    @Override
379    public int getLineDescent(int line) {
380        return mInts.getValue(line, DESCENT);
381    }
382
383    @Override
384    public int getLineStart(int line) {
385        return mInts.getValue(line, START) & START_MASK;
386    }
387
388    @Override
389    public boolean getLineContainsTab(int line) {
390        return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
391    }
392
393    @Override
394    public int getParagraphDirection(int line) {
395        return mInts.getValue(line, DIR) >> DIR_SHIFT;
396    }
397
398    @Override
399    public final Directions getLineDirections(int line) {
400        return mObjects.getValue(line, 0);
401    }
402
403    @Override
404    public int getTopPadding() {
405        return mTopPadding;
406    }
407
408    @Override
409    public int getBottomPadding() {
410        return mBottomPadding;
411    }
412
413    @Override
414    public int getEllipsizedWidth() {
415        return mEllipsizedWidth;
416    }
417
418    private static class ChangeWatcher implements TextWatcher, SpanWatcher {
419        public ChangeWatcher(DynamicLayout layout) {
420            mLayout = new WeakReference<DynamicLayout>(layout);
421        }
422
423        private void reflow(CharSequence s, int where, int before, int after) {
424            DynamicLayout ml = mLayout.get();
425
426            if (ml != null)
427                ml.reflow(s, where, before, after);
428            else if (s instanceof Spannable)
429                ((Spannable) s).removeSpan(this);
430        }
431
432        public void beforeTextChanged(CharSequence s, int where, int before, int after) {
433        }
434
435        public void onTextChanged(CharSequence s, int where, int before, int after) {
436            reflow(s, where, before, after);
437        }
438
439        public void afterTextChanged(Editable s) {
440        }
441
442        public void onSpanAdded(Spannable s, Object o, int start, int end) {
443            if (o instanceof UpdateLayout)
444                reflow(s, start, end - start, end - start);
445        }
446
447        public void onSpanRemoved(Spannable s, Object o, int start, int end) {
448            if (o instanceof UpdateLayout)
449                reflow(s, start, end - start, end - start);
450        }
451
452        public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) {
453            if (o instanceof UpdateLayout) {
454                reflow(s, start, end - start, end - start);
455                reflow(s, nstart, nend - nstart, nend - nstart);
456            }
457        }
458
459        private WeakReference<DynamicLayout> mLayout;
460    }
461
462    @Override
463    public int getEllipsisStart(int line) {
464        if (mEllipsizeAt == null) {
465            return 0;
466        }
467
468        return mInts.getValue(line, ELLIPSIS_START);
469    }
470
471    @Override
472    public int getEllipsisCount(int line) {
473        if (mEllipsizeAt == null) {
474            return 0;
475        }
476
477        return mInts.getValue(line, ELLIPSIS_COUNT);
478    }
479
480    private CharSequence mBase;
481    private CharSequence mDisplay;
482    private ChangeWatcher mWatcher;
483    private boolean mIncludePad;
484    private boolean mEllipsize;
485    private int mEllipsizedWidth;
486    private TextUtils.TruncateAt mEllipsizeAt;
487
488    private PackedIntVector mInts;
489    private PackedObjectVector<Directions> mObjects;
490
491    private int mTopPadding, mBottomPadding;
492
493    private int mMaxLines;
494
495    private static StaticLayout sStaticLayout = new StaticLayout(null);
496
497    private static final Object[] sLock = new Object[0];
498
499    private static final int START = 0;
500    private static final int DIR = START;
501    private static final int TAB = START;
502    private static final int TOP = 1;
503    private static final int DESCENT = 2;
504    private static final int COLUMNS_NORMAL = 3;
505
506    private static final int ELLIPSIS_START = 3;
507    private static final int ELLIPSIS_COUNT = 4;
508    private static final int COLUMNS_ELLIPSIZE = 5;
509
510    private static final int START_MASK = 0x1FFFFFFF;
511    private static final int DIR_SHIFT  = 30;
512    private static final int TAB_MASK   = 0x20000000;
513
514    private static final int ELLIPSIS_UNDEFINED = 0x80000000;
515}
516