• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.text;
18 
19 import android.graphics.Paint;
20 import android.text.style.UpdateLayout;
21 import android.text.style.WrapTogetherSpan;
22 
23 import 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  */
33 public class DynamicLayout
34 extends 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      */
DynamicLayout(CharSequence base, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad)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      */
DynamicLayout(CharSequence base, CharSequence display, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad)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      */
DynamicLayout(CharSequence base, CharSequence display, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)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 
reflow(CharSequence s, int where, int before, int after)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 
314         for (int i = 0; i < n; i++) {
315             ints[START] = reflowed.getLineStart(i) |
316                           (reflowed.getParagraphDirection(i) << DIR_SHIFT) |
317                           (reflowed.getLineContainsTab(i) ? TAB_MASK : 0);
318 
319             int top = reflowed.getLineTop(i) + startv;
320             if (i > 0)
321                 top -= toppad;
322             ints[TOP] = top;
323 
324             int desc = reflowed.getLineDescent(i);
325             if (i == n - 1)
326                 desc += botpad;
327 
328             ints[DESCENT] = desc;
329             objects[0] = reflowed.getLineDirections(i);
330 
331             if (mEllipsize) {
332                 ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
333                 ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i);
334             }
335 
336             mInts.insertAt(startline + i, ints);
337             mObjects.insertAt(startline + i, objects);
338         }
339 
340         synchronized (sLock) {
341             sStaticLayout = reflowed;
342         }
343     }
344 
dump(boolean show)345     private void dump(boolean show) {
346         int n = getLineCount();
347 
348         for (int i = 0; i < n; i++) {
349             System.out.print("line " + i + ": " + getLineStart(i) + " to " + getLineEnd(i) + " ");
350 
351             if (show) {
352                 System.out.print(getText().subSequence(getLineStart(i),
353                                                        getLineEnd(i)));
354             }
355 
356             System.out.println("");
357         }
358 
359         System.out.println("");
360     }
361 
getLineCount()362     public int getLineCount() {
363         return mInts.size() - 1;
364     }
365 
getLineTop(int line)366     public int getLineTop(int line) {
367         return mInts.getValue(line, TOP);
368     }
369 
getLineDescent(int line)370     public int getLineDescent(int line) {
371         return mInts.getValue(line, DESCENT);
372     }
373 
getLineStart(int line)374     public int getLineStart(int line) {
375         return mInts.getValue(line, START) & START_MASK;
376     }
377 
getLineContainsTab(int line)378     public boolean getLineContainsTab(int line) {
379         return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
380     }
381 
getParagraphDirection(int line)382     public int getParagraphDirection(int line) {
383         return mInts.getValue(line, DIR) >> DIR_SHIFT;
384     }
385 
getLineDirections(int line)386     public final Directions getLineDirections(int line) {
387         return mObjects.getValue(line, 0);
388     }
389 
getTopPadding()390     public int getTopPadding() {
391         return mTopPadding;
392     }
393 
getBottomPadding()394     public int getBottomPadding() {
395         return mBottomPadding;
396     }
397 
398     @Override
getEllipsizedWidth()399     public int getEllipsizedWidth() {
400         return mEllipsizedWidth;
401     }
402 
403     private static class ChangeWatcher
404     implements TextWatcher, SpanWatcher
405     {
ChangeWatcher(DynamicLayout layout)406         public ChangeWatcher(DynamicLayout layout) {
407             mLayout = new WeakReference(layout);
408         }
409 
reflow(CharSequence s, int where, int before, int after)410         private void reflow(CharSequence s, int where, int before, int after) {
411             DynamicLayout ml = (DynamicLayout) mLayout.get();
412 
413             if (ml != null)
414                 ml.reflow(s, where, before, after);
415             else if (s instanceof Spannable)
416                 ((Spannable) s).removeSpan(this);
417         }
418 
beforeTextChanged(CharSequence s, int where, int before, int after)419         public void beforeTextChanged(CharSequence s,
420                                       int where, int before, int after) {
421             ;
422         }
423 
onTextChanged(CharSequence s, int where, int before, int after)424         public void onTextChanged(CharSequence s,
425                                   int where, int before, int after) {
426             reflow(s, where, before, after);
427         }
428 
afterTextChanged(Editable s)429         public void afterTextChanged(Editable s) {
430             ;
431         }
432 
onSpanAdded(Spannable s, Object o, int start, int end)433         public void onSpanAdded(Spannable s, Object o, int start, int end) {
434             if (o instanceof UpdateLayout)
435                 reflow(s, start, end - start, end - start);
436         }
437 
onSpanRemoved(Spannable s, Object o, int start, int end)438         public void onSpanRemoved(Spannable s, Object o, int start, int end) {
439             if (o instanceof UpdateLayout)
440                 reflow(s, start, end - start, end - start);
441         }
442 
onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend)443         public void onSpanChanged(Spannable s, Object o, int start, int end,
444                                   int nstart, int nend) {
445             if (o instanceof UpdateLayout) {
446                 reflow(s, start, end - start, end - start);
447                 reflow(s, nstart, nend - nstart, nend - nstart);
448             }
449         }
450 
451         private WeakReference mLayout;
452     }
453 
getEllipsisStart(int line)454     public int getEllipsisStart(int line) {
455         if (mEllipsizeAt == null) {
456             return 0;
457         }
458 
459         return mInts.getValue(line, ELLIPSIS_START);
460     }
461 
getEllipsisCount(int line)462     public int getEllipsisCount(int line) {
463         if (mEllipsizeAt == null) {
464             return 0;
465         }
466 
467         return mInts.getValue(line, ELLIPSIS_COUNT);
468     }
469 
470     private CharSequence mBase;
471     private CharSequence mDisplay;
472     private ChangeWatcher mWatcher;
473     private boolean mIncludePad;
474     private boolean mEllipsize;
475     private int mEllipsizedWidth;
476     private TextUtils.TruncateAt mEllipsizeAt;
477 
478     private PackedIntVector mInts;
479     private PackedObjectVector<Directions> mObjects;
480 
481     private int mTopPadding, mBottomPadding;
482 
483     private static StaticLayout sStaticLayout = new StaticLayout(true);
484     private static Object sLock = new Object();
485 
486     private static final int START = 0;
487     private static final int DIR = START;
488     private static final int TAB = START;
489     private static final int TOP = 1;
490     private static final int DESCENT = 2;
491     private static final int COLUMNS_NORMAL = 3;
492 
493     private static final int ELLIPSIS_START = 3;
494     private static final int ELLIPSIS_COUNT = 4;
495     private static final int COLUMNS_ELLIPSIZE = 5;
496 
497     private static final int START_MASK = 0x1FFFFFFF;
498     private static final int DIR_MASK   = 0xC0000000;
499     private static final int DIR_SHIFT  = 30;
500     private static final int TAB_MASK   = 0x20000000;
501 
502     private static final int ELLIPSIS_UNDEFINED = 0x80000000;
503 }
504