blob: f82c9c4f06e8f8751496d08c683acc1bb90c3ec6 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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) {
Doug Feltcb3791202011-07-07 11:57:48 -070078 this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
79 spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth);
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) {
97 super((ellipsize == null)
98 ? display
99 : (display instanceof Spanned)
100 ? new SpannedEllipsizer(display)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800101 : new Ellipsizer(display),
Doug Feltcb3791202011-07-07 11:57:48 -0700102 paint, width, align, textDir, spacingmult, spacingadd);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800103
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;
Gilles Debunne0a4db3c2011-01-14 12:12:04 -0800114 mEllipsizeAt = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800115 }
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 // Initial state is a single line with 0 characters (0 to 0),
139 // with top at 0 and bottom at whatever is natural, and
140 // undefined ellipsis.
141
142 int[] start;
143
144 if (ellipsize != null) {
145 start = new int[COLUMNS_ELLIPSIZE];
146 start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
147 } else {
148 start = new int[COLUMNS_NORMAL];
149 }
150
151 Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT };
152
153 Paint.FontMetricsInt fm = paint.getFontMetricsInt();
154 int asc = fm.ascent;
155 int desc = fm.descent;
156
157 start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT;
158 start[TOP] = 0;
159 start[DESCENT] = desc;
160 mInts.insertAt(0, start);
161
162 start[TOP] = desc - asc;
163 mInts.insertAt(1, start);
164
165 mObjects.insertAt(0, dirs);
166
167 // Update from 0 characters to whatever the real text is
168
169 reflow(base, 0, 0, base.length());
170
171 if (base instanceof Spannable) {
172 if (mWatcher == null)
173 mWatcher = new ChangeWatcher(this);
174
175 // Strip out any watchers for other DynamicLayouts.
176 Spannable sp = (Spannable) base;
177 ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class);
178 for (int i = 0; i < spans.length; i++)
179 sp.removeSpan(spans[i]);
180
181 sp.setSpan(mWatcher, 0, base.length(),
182 Spannable.SPAN_INCLUSIVE_INCLUSIVE |
183 (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT));
184 }
185 }
186
187 private void reflow(CharSequence s, int where, int before, int after) {
188 if (s != mBase)
189 return;
190
191 CharSequence text = mDisplay;
192 int len = text.length();
193
194 // seek back to the start of the paragraph
195
196 int find = TextUtils.lastIndexOf(text, '\n', where - 1);
197 if (find < 0)
198 find = 0;
199 else
200 find = find + 1;
201
202 {
203 int diff = where - find;
204 before += diff;
205 after += diff;
206 where -= diff;
207 }
208
209 // seek forward to the end of the paragraph
210
211 int look = TextUtils.indexOf(text, '\n', where + after);
212 if (look < 0)
213 look = len;
214 else
215 look++; // we want the index after the \n
216
217 int change = look - (where + after);
218 before += change;
219 after += change;
220
221 // seek further out to cover anything that is forced to wrap together
222
223 if (text instanceof Spanned) {
224 Spanned sp = (Spanned) text;
225 boolean again;
226
227 do {
228 again = false;
229
230 Object[] force = sp.getSpans(where, where + after,
231 WrapTogetherSpan.class);
232
233 for (int i = 0; i < force.length; i++) {
234 int st = sp.getSpanStart(force[i]);
235 int en = sp.getSpanEnd(force[i]);
236
237 if (st < where) {
238 again = true;
239
240 int diff = where - st;
241 before += diff;
242 after += diff;
243 where -= diff;
244 }
245
246 if (en > where + after) {
247 again = true;
248
249 int diff = en - (where + after);
250 before += diff;
251 after += diff;
252 }
253 }
254 } while (again);
255 }
256
257 // find affected region of old layout
258
259 int startline = getLineForOffset(where);
260 int startv = getLineTop(startline);
261
262 int endline = getLineForOffset(where + before);
263 if (where + after == len)
264 endline = getLineCount();
265 int endv = getLineTop(endline);
266 boolean islast = (endline == getLineCount());
267
268 // generate new layout for affected text
269
270 StaticLayout reflowed;
Amith Yamasanib724c342011-08-13 07:43:07 -0700271
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800272 synchronized (sLock) {
Amith Yamasanib724c342011-08-13 07:43:07 -0700273 reflowed = sStaticLayout;
274 sStaticLayout = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800275 }
276
Romain Guye5ea4402011-08-01 14:01:37 -0700277 if (reflowed == null) {
Fabrice Di Meglio09175732011-09-25 16:48:04 -0700278 reflowed = new StaticLayout(null);
Romain Guye5ea4402011-08-01 14:01:37 -0700279 } else {
280 reflowed.prepare();
281 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800282
283 reflowed.generate(text, where, where + after,
Doug Feltcb3791202011-07-07 11:57:48 -0700284 getPaint(), getWidth(), getAlignment(), getTextDirectionHeuristic(),
Gilles Debunne0a4db3c2011-01-14 12:12:04 -0800285 getSpacingMultiplier(), getSpacingAdd(),
286 false, true, mEllipsizedWidth, mEllipsizeAt);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800287 int n = reflowed.getLineCount();
288
289 // If the new layout has a blank line at the end, but it is not
290 // the very end of the buffer, then we already have a line that
291 // starts there, so disregard the blank line.
292
293 if (where + after != len &&
294 reflowed.getLineStart(n - 1) == where + after)
295 n--;
296
297 // remove affected lines from old layout
298
299 mInts.deleteAt(startline, endline - startline);
300 mObjects.deleteAt(startline, endline - startline);
301
302 // adjust offsets in layout for new height and offsets
303
304 int ht = reflowed.getLineTop(n);
305 int toppad = 0, botpad = 0;
306
307 if (mIncludePad && startline == 0) {
308 toppad = reflowed.getTopPadding();
309 mTopPadding = toppad;
310 ht -= toppad;
311 }
312 if (mIncludePad && islast) {
313 botpad = reflowed.getBottomPadding();
314 mBottomPadding = botpad;
315 ht += botpad;
316 }
317
318 mInts.adjustValuesBelow(startline, START, after - before);
319 mInts.adjustValuesBelow(startline, TOP, startv - endv + ht);
320
321 // insert new layout
322
323 int[] ints;
324
325 if (mEllipsize) {
326 ints = new int[COLUMNS_ELLIPSIZE];
327 ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
328 } else {
329 ints = new int[COLUMNS_NORMAL];
330 }
331
332 Directions[] objects = new Directions[1];
333
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800334 for (int i = 0; i < n; i++) {
335 ints[START] = reflowed.getLineStart(i) |
336 (reflowed.getParagraphDirection(i) << DIR_SHIFT) |
337 (reflowed.getLineContainsTab(i) ? TAB_MASK : 0);
338
339 int top = reflowed.getLineTop(i) + startv;
340 if (i > 0)
341 top -= toppad;
342 ints[TOP] = top;
343
344 int desc = reflowed.getLineDescent(i);
345 if (i == n - 1)
346 desc += botpad;
347
348 ints[DESCENT] = desc;
349 objects[0] = reflowed.getLineDirections(i);
350
351 if (mEllipsize) {
352 ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
353 ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i);
354 }
355
356 mInts.insertAt(startline + i, ints);
357 mObjects.insertAt(startline + i, objects);
358 }
359
360 synchronized (sLock) {
Amith Yamasanib724c342011-08-13 07:43:07 -0700361 sStaticLayout = reflowed;
Romain Guye5ea4402011-08-01 14:01:37 -0700362 reflowed.finish();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800363 }
364 }
365
Gilles Debunned6e568c2011-01-25 10:14:43 -0800366 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800367 public int getLineCount() {
368 return mInts.size() - 1;
369 }
370
Gilles Debunned6e568c2011-01-25 10:14:43 -0800371 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800372 public int getLineTop(int line) {
373 return mInts.getValue(line, TOP);
374 }
375
Gilles Debunned6e568c2011-01-25 10:14:43 -0800376 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800377 public int getLineDescent(int line) {
378 return mInts.getValue(line, DESCENT);
379 }
380
Gilles Debunned6e568c2011-01-25 10:14:43 -0800381 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800382 public int getLineStart(int line) {
383 return mInts.getValue(line, START) & START_MASK;
384 }
385
Gilles Debunned6e568c2011-01-25 10:14:43 -0800386 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800387 public boolean getLineContainsTab(int line) {
388 return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
389 }
390
Gilles Debunned6e568c2011-01-25 10:14:43 -0800391 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800392 public int getParagraphDirection(int line) {
393 return mInts.getValue(line, DIR) >> DIR_SHIFT;
394 }
395
Gilles Debunned6e568c2011-01-25 10:14:43 -0800396 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800397 public final Directions getLineDirections(int line) {
398 return mObjects.getValue(line, 0);
399 }
400
Gilles Debunned6e568c2011-01-25 10:14:43 -0800401 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800402 public int getTopPadding() {
403 return mTopPadding;
404 }
405
Gilles Debunned6e568c2011-01-25 10:14:43 -0800406 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800407 public int getBottomPadding() {
408 return mBottomPadding;
409 }
410
411 @Override
412 public int getEllipsizedWidth() {
413 return mEllipsizedWidth;
414 }
415
Gilles Debunne0a4db3c2011-01-14 12:12:04 -0800416 private static class ChangeWatcher implements TextWatcher, SpanWatcher {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800417 public ChangeWatcher(DynamicLayout layout) {
Gilles Debunned6e568c2011-01-25 10:14:43 -0800418 mLayout = new WeakReference<DynamicLayout>(layout);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800419 }
420
421 private void reflow(CharSequence s, int where, int before, int after) {
Gilles Debunned6e568c2011-01-25 10:14:43 -0800422 DynamicLayout ml = mLayout.get();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800423
424 if (ml != null)
425 ml.reflow(s, where, before, after);
426 else if (s instanceof Spannable)
427 ((Spannable) s).removeSpan(this);
428 }
429
Gilles Debunne0a4db3c2011-01-14 12:12:04 -0800430 public void beforeTextChanged(CharSequence s, int where, int before, int after) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800431 }
432
Gilles Debunne0a4db3c2011-01-14 12:12:04 -0800433 public void onTextChanged(CharSequence s, int where, int before, int after) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800434 reflow(s, where, before, after);
435 }
436
437 public void afterTextChanged(Editable s) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800438 }
439
440 public void onSpanAdded(Spannable s, Object o, int start, int end) {
441 if (o instanceof UpdateLayout)
442 reflow(s, start, end - start, end - start);
443 }
444
445 public void onSpanRemoved(Spannable s, Object o, int start, int end) {
446 if (o instanceof UpdateLayout)
447 reflow(s, start, end - start, end - start);
448 }
449
Gilles Debunne0a4db3c2011-01-14 12:12:04 -0800450 public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800451 if (o instanceof UpdateLayout) {
452 reflow(s, start, end - start, end - start);
453 reflow(s, nstart, nend - nstart, nend - nstart);
454 }
455 }
456
Gilles Debunned6e568c2011-01-25 10:14:43 -0800457 private WeakReference<DynamicLayout> mLayout;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800458 }
459
Gilles Debunned6e568c2011-01-25 10:14:43 -0800460 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800461 public int getEllipsisStart(int line) {
462 if (mEllipsizeAt == null) {
463 return 0;
464 }
465
466 return mInts.getValue(line, ELLIPSIS_START);
467 }
468
Gilles Debunned6e568c2011-01-25 10:14:43 -0800469 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800470 public int getEllipsisCount(int line) {
471 if (mEllipsizeAt == null) {
472 return 0;
473 }
474
475 return mInts.getValue(line, ELLIPSIS_COUNT);
476 }
477
478 private CharSequence mBase;
479 private CharSequence mDisplay;
480 private ChangeWatcher mWatcher;
481 private boolean mIncludePad;
482 private boolean mEllipsize;
483 private int mEllipsizedWidth;
484 private TextUtils.TruncateAt mEllipsizeAt;
485
486 private PackedIntVector mInts;
487 private PackedObjectVector<Directions> mObjects;
488
489 private int mTopPadding, mBottomPadding;
490
Fabrice Di Meglio09175732011-09-25 16:48:04 -0700491 private static StaticLayout sStaticLayout = new StaticLayout(null);
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700492
Romain Guye5ea4402011-08-01 14:01:37 -0700493 private static final Object[] sLock = new Object[0];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800494
495 private static final int START = 0;
496 private static final int DIR = START;
497 private static final int TAB = START;
498 private static final int TOP = 1;
499 private static final int DESCENT = 2;
500 private static final int COLUMNS_NORMAL = 3;
501
502 private static final int ELLIPSIS_START = 3;
503 private static final int ELLIPSIS_COUNT = 4;
504 private static final int COLUMNS_ELLIPSIZE = 5;
505
506 private static final int START_MASK = 0x1FFFFFFF;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800507 private static final int DIR_SHIFT = 30;
508 private static final int TAB_MASK = 0x20000000;
509
510 private static final int ELLIPSIS_UNDEFINED = 0x80000000;
511}