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