blob: c7a5fce26c3e9e126e73aacf5d312100b8559d97 [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;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +090020import android.graphics.Rect;
21import android.text.style.ReplacementSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080022import android.text.style.UpdateLayout;
23import android.text.style.WrapTogetherSpan;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +090024import android.util.ArraySet;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080025
Siyamed Sinired09ae12016-02-16 14:36:26 -080026import com.android.internal.annotations.VisibleForTesting;
Gilles Debunne33b7de852012-03-12 11:57:48 -070027import com.android.internal.util.ArrayUtils;
Adam Lesinski776abc22014-03-07 11:30:59 -050028import com.android.internal.util.GrowingArrayUtils;
Gilles Debunne33b7de852012-03-12 11:57:48 -070029
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080030import java.lang.ref.WeakReference;
31
32/**
33 * DynamicLayout is a text layout that updates itself as the text is edited.
34 * <p>This is used by widgets to control text layout. You should not need
35 * to use this class directly unless you are implementing your own widget
36 * or custom display object, or need to call
37 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
38 * Canvas.drawText()} directly.</p>
39 */
Gilles Debunne33b7de852012-03-12 11:57:48 -070040public class DynamicLayout extends Layout
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080041{
42 private static final int PRIORITY = 128;
Gilles Debunne71afc392012-05-10 10:24:20 -070043 private static final int BLOCK_MINIMUM_CHARACTER_LENGTH = 400;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080044
45 /**
46 * Make a layout for the specified text that will be updated as
47 * the text is changed.
48 */
49 public DynamicLayout(CharSequence base,
50 TextPaint paint,
51 int width, Alignment align,
52 float spacingmult, float spacingadd,
53 boolean includepad) {
54 this(base, base, paint, width, align, spacingmult, spacingadd,
55 includepad);
56 }
57
58 /**
59 * Make a layout for the transformed text (password transformation
60 * being the primary example of a transformation)
61 * that will be updated as the base text is changed.
62 */
63 public DynamicLayout(CharSequence base, CharSequence display,
64 TextPaint paint,
65 int width, Alignment align,
66 float spacingmult, float spacingadd,
67 boolean includepad) {
68 this(base, display, paint, width, align, spacingmult, spacingadd,
69 includepad, null, 0);
70 }
71
72 /**
73 * Make a layout for the transformed text (password transformation
74 * being the primary example of a transformation)
75 * that will be updated as the base text is changed.
76 * If ellipsize is non-null, the Layout will ellipsize the text
77 * down to ellipsizedWidth.
78 */
79 public DynamicLayout(CharSequence base, CharSequence display,
80 TextPaint paint,
81 int width, Alignment align,
82 float spacingmult, float spacingadd,
83 boolean includepad,
84 TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
Doug Feltcb3791202011-07-07 11:57:48 -070085 this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
Roozbeh Pournader95c7a132015-05-12 12:01:06 -070086 spacingmult, spacingadd, includepad,
87 StaticLayout.BREAK_STRATEGY_SIMPLE, StaticLayout.HYPHENATION_FREQUENCY_NONE,
Seigo Nonaka4b4730d2017-03-31 09:42:16 -070088 Layout.JUSTIFICATION_MODE_NONE, ellipsize, ellipsizedWidth);
Doug Feltcb3791202011-07-07 11:57:48 -070089 }
90
91 /**
92 * Make a layout for the transformed text (password transformation
93 * being the primary example of a transformation)
94 * that will be updated as the base text is changed.
95 * If ellipsize is non-null, the Layout will ellipsize the text
96 * down to ellipsizedWidth.
97 * *
98 * *@hide
99 */
100 public DynamicLayout(CharSequence base, CharSequence display,
101 TextPaint paint,
102 int width, Alignment align, TextDirectionHeuristic textDir,
103 float spacingmult, float spacingadd,
Roozbeh Pournader95c7a132015-05-12 12:01:06 -0700104 boolean includepad, int breakStrategy, int hyphenationFrequency,
Seigo Nonaka4b4730d2017-03-31 09:42:16 -0700105 int justificationMode, TextUtils.TruncateAt ellipsize,
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900106 int ellipsizedWidth) {
Doug Feltcb3791202011-07-07 11:57:48 -0700107 super((ellipsize == null)
108 ? display
109 : (display instanceof Spanned)
110 ? new SpannedEllipsizer(display)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800111 : new Ellipsizer(display),
Doug Feltcb3791202011-07-07 11:57:48 -0700112 paint, width, align, textDir, spacingmult, spacingadd);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800113
114 mBase = base;
115 mDisplay = display;
116
117 if (ellipsize != null) {
118 mInts = new PackedIntVector(COLUMNS_ELLIPSIZE);
119 mEllipsizedWidth = ellipsizedWidth;
120 mEllipsizeAt = ellipsize;
121 } else {
122 mInts = new PackedIntVector(COLUMNS_NORMAL);
123 mEllipsizedWidth = width;
Gilles Debunne0a4db3c2011-01-14 12:12:04 -0800124 mEllipsizeAt = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800125 }
126
127 mObjects = new PackedObjectVector<Directions>(1);
128
129 mIncludePad = includepad;
Raph Levien39b4db72015-03-25 13:18:20 -0700130 mBreakStrategy = breakStrategy;
Seigo Nonaka4b4730d2017-03-31 09:42:16 -0700131 mJustificationMode = justificationMode;
Roozbeh Pournader95c7a132015-05-12 12:01:06 -0700132 mHyphenationFrequency = hyphenationFrequency;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800133
134 /*
135 * This is annoying, but we can't refer to the layout until
136 * superclass construction is finished, and the superclass
137 * constructor wants the reference to the display text.
138 *
139 * This will break if the superclass constructor ever actually
140 * cares about the content instead of just holding the reference.
141 */
142 if (ellipsize != null) {
143 Ellipsizer e = (Ellipsizer) getText();
144
145 e.mLayout = this;
146 e.mWidth = ellipsizedWidth;
147 e.mMethod = ellipsize;
148 mEllipsize = true;
149 }
150
151 // Initial state is a single line with 0 characters (0 to 0),
152 // with top at 0 and bottom at whatever is natural, and
153 // undefined ellipsis.
154
155 int[] start;
156
157 if (ellipsize != null) {
158 start = new int[COLUMNS_ELLIPSIZE];
159 start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
160 } else {
161 start = new int[COLUMNS_NORMAL];
162 }
163
164 Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT };
165
166 Paint.FontMetricsInt fm = paint.getFontMetricsInt();
167 int asc = fm.ascent;
168 int desc = fm.descent;
169
170 start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT;
171 start[TOP] = 0;
172 start[DESCENT] = desc;
173 mInts.insertAt(0, start);
174
175 start[TOP] = desc - asc;
176 mInts.insertAt(1, start);
177
178 mObjects.insertAt(0, dirs);
179
180 // Update from 0 characters to whatever the real text is
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800181 reflow(base, 0, 0, base.length());
182
183 if (base instanceof Spannable) {
184 if (mWatcher == null)
185 mWatcher = new ChangeWatcher(this);
186
187 // Strip out any watchers for other DynamicLayouts.
188 Spannable sp = (Spannable) base;
189 ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class);
190 for (int i = 0; i < spans.length; i++)
191 sp.removeSpan(spans[i]);
192
193 sp.setSpan(mWatcher, 0, base.length(),
194 Spannable.SPAN_INCLUSIVE_INCLUSIVE |
195 (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT));
196 }
197 }
198
199 private void reflow(CharSequence s, int where, int before, int after) {
200 if (s != mBase)
201 return;
202
203 CharSequence text = mDisplay;
204 int len = text.length();
205
206 // seek back to the start of the paragraph
207
208 int find = TextUtils.lastIndexOf(text, '\n', where - 1);
209 if (find < 0)
210 find = 0;
211 else
212 find = find + 1;
213
214 {
215 int diff = where - find;
216 before += diff;
217 after += diff;
218 where -= diff;
219 }
220
221 // seek forward to the end of the paragraph
222
223 int look = TextUtils.indexOf(text, '\n', where + after);
224 if (look < 0)
225 look = len;
226 else
227 look++; // we want the index after the \n
228
229 int change = look - (where + after);
230 before += change;
231 after += change;
232
233 // seek further out to cover anything that is forced to wrap together
234
235 if (text instanceof Spanned) {
236 Spanned sp = (Spanned) text;
237 boolean again;
238
239 do {
240 again = false;
241
242 Object[] force = sp.getSpans(where, where + after,
243 WrapTogetherSpan.class);
244
245 for (int i = 0; i < force.length; i++) {
246 int st = sp.getSpanStart(force[i]);
247 int en = sp.getSpanEnd(force[i]);
248
249 if (st < where) {
250 again = true;
251
252 int diff = where - st;
253 before += diff;
254 after += diff;
255 where -= diff;
256 }
257
258 if (en > where + after) {
259 again = true;
260
261 int diff = en - (where + after);
262 before += diff;
263 after += diff;
264 }
265 }
266 } while (again);
267 }
268
269 // find affected region of old layout
270
271 int startline = getLineForOffset(where);
272 int startv = getLineTop(startline);
273
274 int endline = getLineForOffset(where + before);
275 if (where + after == len)
276 endline = getLineCount();
277 int endv = getLineTop(endline);
278 boolean islast = (endline == getLineCount());
279
280 // generate new layout for affected text
281
282 StaticLayout reflowed;
Raph Leviend3ab6922015-03-02 14:30:53 -0800283 StaticLayout.Builder b;
Amith Yamasanib724c342011-08-13 07:43:07 -0700284
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800285 synchronized (sLock) {
Amith Yamasanib724c342011-08-13 07:43:07 -0700286 reflowed = sStaticLayout;
Raph Leviend3ab6922015-03-02 14:30:53 -0800287 b = sBuilder;
Amith Yamasanib724c342011-08-13 07:43:07 -0700288 sStaticLayout = null;
Raph Leviend3ab6922015-03-02 14:30:53 -0800289 sBuilder = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800290 }
291
Romain Guye5ea4402011-08-01 14:01:37 -0700292 if (reflowed == null) {
Fabrice Di Meglio09175732011-09-25 16:48:04 -0700293 reflowed = new StaticLayout(null);
Raph Levienebd66ca2015-04-30 15:27:57 -0700294 b = StaticLayout.Builder.obtain(text, where, where + after, getPaint(), getWidth());
Romain Guye5ea4402011-08-01 14:01:37 -0700295 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800296
Raph Leviend3ab6922015-03-02 14:30:53 -0800297 b.setText(text, where, where + after)
298 .setPaint(getPaint())
299 .setWidth(getWidth())
Raph Leviena6a08282015-06-03 13:20:45 -0700300 .setTextDirection(getTextDirectionHeuristic())
Raph Levien531c30c2015-04-30 16:29:59 -0700301 .setLineSpacing(getSpacingAdd(), getSpacingMultiplier())
Raph Leviend3ab6922015-03-02 14:30:53 -0800302 .setEllipsizedWidth(mEllipsizedWidth)
Raph Levien39b4db72015-03-25 13:18:20 -0700303 .setEllipsize(mEllipsizeAt)
Roozbeh Pournader95c7a132015-05-12 12:01:06 -0700304 .setBreakStrategy(mBreakStrategy)
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900305 .setHyphenationFrequency(mHyphenationFrequency)
Seigo Nonaka4b4730d2017-03-31 09:42:16 -0700306 .setJustificationMode(mJustificationMode);
Raph Leviend3ab6922015-03-02 14:30:53 -0800307 reflowed.generate(b, false, true);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800308 int n = reflowed.getLineCount();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800309 // If the new layout has a blank line at the end, but it is not
310 // the very end of the buffer, then we already have a line that
311 // starts there, so disregard the blank line.
312
Gilles Debunne71afc392012-05-10 10:24:20 -0700313 if (where + after != len && reflowed.getLineStart(n - 1) == where + after)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800314 n--;
315
316 // remove affected lines from old layout
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800317 mInts.deleteAt(startline, endline - startline);
318 mObjects.deleteAt(startline, endline - startline);
319
320 // adjust offsets in layout for new height and offsets
321
322 int ht = reflowed.getLineTop(n);
323 int toppad = 0, botpad = 0;
324
325 if (mIncludePad && startline == 0) {
326 toppad = reflowed.getTopPadding();
327 mTopPadding = toppad;
328 ht -= toppad;
329 }
330 if (mIncludePad && islast) {
331 botpad = reflowed.getBottomPadding();
332 mBottomPadding = botpad;
333 ht += botpad;
334 }
335
336 mInts.adjustValuesBelow(startline, START, after - before);
337 mInts.adjustValuesBelow(startline, TOP, startv - endv + ht);
338
339 // insert new layout
340
341 int[] ints;
342
343 if (mEllipsize) {
344 ints = new int[COLUMNS_ELLIPSIZE];
345 ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
346 } else {
347 ints = new int[COLUMNS_NORMAL];
348 }
349
350 Directions[] objects = new Directions[1];
351
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800352 for (int i = 0; i < n; i++) {
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900353 final int start = reflowed.getLineStart(i);
354 ints[START] = start;
355 ints[DIR] |= reflowed.getParagraphDirection(i) << DIR_SHIFT;
356 ints[TAB] |= reflowed.getLineContainsTab(i) ? TAB_MASK : 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800357
358 int top = reflowed.getLineTop(i) + startv;
359 if (i > 0)
360 top -= toppad;
361 ints[TOP] = top;
362
363 int desc = reflowed.getLineDescent(i);
364 if (i == n - 1)
365 desc += botpad;
366
367 ints[DESCENT] = desc;
368 objects[0] = reflowed.getLineDirections(i);
369
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900370 final int end = (i == n - 1) ? where + after : reflowed.getLineStart(i + 1);
371 ints[HYPHEN] = reflowed.getHyphen(i) & HYPHEN_MASK;
372 ints[MAY_PROTRUDE_FROM_TOP_OR_BOTTOM] |=
373 contentMayProtrudeFromLineTopOrBottom(text, start, end) ?
374 MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK : 0;
Raph Levien26d443a2015-03-30 14:18:32 -0700375
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800376 if (mEllipsize) {
377 ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
378 ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i);
379 }
380
381 mInts.insertAt(startline + i, ints);
382 mObjects.insertAt(startline + i, objects);
383 }
384
Gilles Debunne71afc392012-05-10 10:24:20 -0700385 updateBlocks(startline, endline - 1, n);
386
Raph Leviend3ab6922015-03-02 14:30:53 -0800387 b.finish();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800388 synchronized (sLock) {
Amith Yamasanib724c342011-08-13 07:43:07 -0700389 sStaticLayout = reflowed;
Raph Leviend3ab6922015-03-02 14:30:53 -0800390 sBuilder = b;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800391 }
392 }
393
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900394 private boolean contentMayProtrudeFromLineTopOrBottom(CharSequence text, int start, int end) {
395 if (text instanceof Spanned) {
396 final Spanned spanned = (Spanned) text;
397 if (spanned.getSpans(start, end, ReplacementSpan.class).length > 0) {
398 return true;
399 }
400 }
401 // Spans other than ReplacementSpan can be ignored because line top and bottom are
402 // disjunction of all tops and bottoms, although it's not optimal.
403 final Paint paint = getPaint();
404 paint.getTextBounds(text, start, end, mTempRect);
405 final Paint.FontMetricsInt fm = paint.getFontMetricsInt();
406 return mTempRect.top < fm.top || mTempRect.bottom > fm.bottom;
407 }
408
Gilles Debunne33b7de852012-03-12 11:57:48 -0700409 /**
Gilles Debunne71afc392012-05-10 10:24:20 -0700410 * Create the initial block structure, cutting the text into blocks of at least
411 * BLOCK_MINIMUM_CHARACTER_SIZE characters, aligned on the ends of paragraphs.
412 */
413 private void createBlocks() {
414 int offset = BLOCK_MINIMUM_CHARACTER_LENGTH;
415 mNumberOfBlocks = 0;
416 final CharSequence text = mDisplay;
417
418 while (true) {
419 offset = TextUtils.indexOf(text, '\n', offset);
420 if (offset < 0) {
421 addBlockAtOffset(text.length());
422 break;
423 } else {
424 addBlockAtOffset(offset);
425 offset += BLOCK_MINIMUM_CHARACTER_LENGTH;
426 }
427 }
428
429 // mBlockIndices and mBlockEndLines should have the same length
430 mBlockIndices = new int[mBlockEndLines.length];
431 for (int i = 0; i < mBlockEndLines.length; i++) {
432 mBlockIndices[i] = INVALID_BLOCK_INDEX;
433 }
434 }
435
436 /**
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900437 * @hide
438 */
439 public ArraySet<Integer> getBlocksAlwaysNeedToBeRedrawn() {
440 return mBlocksAlwaysNeedToBeRedrawn;
441 }
442
443 private void updateAlwaysNeedsToBeRedrawn(int blockIndex) {
444 int startLine = blockIndex == 0 ? 0 : (mBlockEndLines[blockIndex - 1] + 1);
445 int endLine = mBlockEndLines[blockIndex];
446 for (int i = startLine; i <= endLine; i++) {
447 if (getContentMayProtrudeFromTopOrBottom(i)) {
448 if (mBlocksAlwaysNeedToBeRedrawn == null) {
449 mBlocksAlwaysNeedToBeRedrawn = new ArraySet<>();
450 }
451 mBlocksAlwaysNeedToBeRedrawn.add(blockIndex);
452 return;
453 }
454 }
455 if (mBlocksAlwaysNeedToBeRedrawn != null) {
456 mBlocksAlwaysNeedToBeRedrawn.remove(blockIndex);
457 }
458 }
459
460 /**
Gilles Debunne71afc392012-05-10 10:24:20 -0700461 * Create a new block, ending at the specified character offset.
462 * A block will actually be created only if has at least one line, i.e. this offset is
463 * not on the end line of the previous block.
464 */
465 private void addBlockAtOffset(int offset) {
466 final int line = getLineForOffset(offset);
Gilles Debunne71afc392012-05-10 10:24:20 -0700467 if (mBlockEndLines == null) {
468 // Initial creation of the array, no test on previous block ending line
Adam Lesinski776abc22014-03-07 11:30:59 -0500469 mBlockEndLines = ArrayUtils.newUnpaddedIntArray(1);
Gilles Debunne71afc392012-05-10 10:24:20 -0700470 mBlockEndLines[mNumberOfBlocks] = line;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900471 updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
Gilles Debunne71afc392012-05-10 10:24:20 -0700472 mNumberOfBlocks++;
473 return;
474 }
475
476 final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1];
477 if (line > previousBlockEndLine) {
Adam Lesinski776abc22014-03-07 11:30:59 -0500478 mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line);
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900479 updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
Gilles Debunne71afc392012-05-10 10:24:20 -0700480 mNumberOfBlocks++;
481 }
482 }
483
484 /**
Gilles Debunne33b7de852012-03-12 11:57:48 -0700485 * This method is called every time the layout is reflowed after an edition.
486 * It updates the internal block data structure. The text is split in blocks
487 * of contiguous lines, with at least one block for the entire text.
488 * When a range of lines is edited, new blocks (from 0 to 3 depending on the
489 * overlap structure) will replace the set of overlapping blocks.
490 * Blocks are listed in order and are represented by their ending line number.
491 * An index is associated to each block (which will be used by display lists),
492 * this class simply invalidates the index of blocks overlapping a modification.
493 *
494 * @param startLine the first line of the range of modified lines
495 * @param endLine the last line of the range, possibly equal to startLine, lower
496 * than getLineCount()
497 * @param newLineCount the number of lines that will replace the range, possibly 0
Gilles Debunne1e130b22012-03-14 17:40:42 -0700498 *
499 * @hide
Gilles Debunne33b7de852012-03-12 11:57:48 -0700500 */
Siyamed Sinired09ae12016-02-16 14:36:26 -0800501 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
502 public void updateBlocks(int startLine, int endLine, int newLineCount) {
Gilles Debunne71afc392012-05-10 10:24:20 -0700503 if (mBlockEndLines == null) {
504 createBlocks();
505 return;
506 }
507
Gilles Debunne33b7de852012-03-12 11:57:48 -0700508 int firstBlock = -1;
509 int lastBlock = -1;
510 for (int i = 0; i < mNumberOfBlocks; i++) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700511 if (mBlockEndLines[i] >= startLine) {
Gilles Debunne33b7de852012-03-12 11:57:48 -0700512 firstBlock = i;
513 break;
514 }
515 }
516 for (int i = firstBlock; i < mNumberOfBlocks; i++) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700517 if (mBlockEndLines[i] >= endLine) {
Gilles Debunne33b7de852012-03-12 11:57:48 -0700518 lastBlock = i;
519 break;
520 }
521 }
Gilles Debunne157aafc2012-04-19 17:21:57 -0700522 final int lastBlockEndLine = mBlockEndLines[lastBlock];
Gilles Debunne33b7de852012-03-12 11:57:48 -0700523
524 boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
Gilles Debunne157aafc2012-04-19 17:21:57 -0700525 mBlockEndLines[firstBlock - 1] + 1);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700526 boolean createBlock = newLineCount > 0;
Gilles Debunne157aafc2012-04-19 17:21:57 -0700527 boolean createBlockAfter = endLine < mBlockEndLines[lastBlock];
Gilles Debunne33b7de852012-03-12 11:57:48 -0700528
529 int numAddedBlocks = 0;
530 if (createBlockBefore) numAddedBlocks++;
531 if (createBlock) numAddedBlocks++;
532 if (createBlockAfter) numAddedBlocks++;
533
534 final int numRemovedBlocks = lastBlock - firstBlock + 1;
535 final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks;
536
537 if (newNumberOfBlocks == 0) {
538 // Even when text is empty, there is actually one line and hence one block
Gilles Debunne157aafc2012-04-19 17:21:57 -0700539 mBlockEndLines[0] = 0;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700540 mBlockIndices[0] = INVALID_BLOCK_INDEX;
541 mNumberOfBlocks = 1;
542 return;
543 }
544
Gilles Debunne157aafc2012-04-19 17:21:57 -0700545 if (newNumberOfBlocks > mBlockEndLines.length) {
Adam Lesinski776abc22014-03-07 11:30:59 -0500546 int[] blockEndLines = ArrayUtils.newUnpaddedIntArray(
547 Math.max(mBlockEndLines.length * 2, newNumberOfBlocks));
548 int[] blockIndices = new int[blockEndLines.length];
Gilles Debunne157aafc2012-04-19 17:21:57 -0700549 System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700550 System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock);
Gilles Debunne157aafc2012-04-19 17:21:57 -0700551 System.arraycopy(mBlockEndLines, lastBlock + 1,
552 blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700553 System.arraycopy(mBlockIndices, lastBlock + 1,
554 blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
Gilles Debunne157aafc2012-04-19 17:21:57 -0700555 mBlockEndLines = blockEndLines;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700556 mBlockIndices = blockIndices;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900557 } else if (numAddedBlocks + numRemovedBlocks != 0) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700558 System.arraycopy(mBlockEndLines, lastBlock + 1,
559 mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700560 System.arraycopy(mBlockIndices, lastBlock + 1,
561 mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
562 }
563
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900564 if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) {
565 final ArraySet<Integer> set = new ArraySet<>();
566 for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) {
567 Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i);
568 if (block > firstBlock) {
569 block += numAddedBlocks - numRemovedBlocks;
570 }
571 set.add(block);
572 }
573 mBlocksAlwaysNeedToBeRedrawn = set;
574 }
575
Gilles Debunne33b7de852012-03-12 11:57:48 -0700576 mNumberOfBlocks = newNumberOfBlocks;
Raph Levien63b3eb62013-10-03 13:48:04 -0700577 int newFirstChangedBlock;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700578 final int deltaLines = newLineCount - (endLine - startLine + 1);
Sangkyu Lee955beb22012-12-10 15:47:00 +0900579 if (deltaLines != 0) {
580 // Display list whose index is >= mIndexFirstChangedBlock is valid
581 // but it needs to update its drawing location.
Raph Levien63b3eb62013-10-03 13:48:04 -0700582 newFirstChangedBlock = firstBlock + numAddedBlocks;
583 for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) {
Sangkyu Lee955beb22012-12-10 15:47:00 +0900584 mBlockEndLines[i] += deltaLines;
585 }
586 } else {
Raph Levien63b3eb62013-10-03 13:48:04 -0700587 newFirstChangedBlock = mNumberOfBlocks;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700588 }
Raph Levien63b3eb62013-10-03 13:48:04 -0700589 mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700590
591 int blockIndex = firstBlock;
592 if (createBlockBefore) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700593 mBlockEndLines[blockIndex] = startLine - 1;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900594 updateAlwaysNeedsToBeRedrawn(blockIndex);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700595 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
596 blockIndex++;
597 }
598
599 if (createBlock) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700600 mBlockEndLines[blockIndex] = startLine + newLineCount - 1;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900601 updateAlwaysNeedsToBeRedrawn(blockIndex);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700602 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
603 blockIndex++;
604 }
605
606 if (createBlockAfter) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700607 mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900608 updateAlwaysNeedsToBeRedrawn(blockIndex);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700609 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
610 }
611 }
612
613 /**
Roozbeh Pournaderfc778692017-05-17 14:24:02 -0700614 * This method is used for test purposes only.
Gilles Debunne1e130b22012-03-14 17:40:42 -0700615 * @hide
616 */
Siyamed Sinired09ae12016-02-16 14:36:26 -0800617 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
Roozbeh Pournaderfc778692017-05-17 14:24:02 -0700618 public void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks,
619 int totalLines) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700620 mBlockEndLines = new int[blockEndLines.length];
Gilles Debunne1e130b22012-03-14 17:40:42 -0700621 mBlockIndices = new int[blockIndices.length];
Gilles Debunne157aafc2012-04-19 17:21:57 -0700622 System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length);
Gilles Debunne1e130b22012-03-14 17:40:42 -0700623 System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length);
624 mNumberOfBlocks = numberOfBlocks;
Roozbeh Pournaderfc778692017-05-17 14:24:02 -0700625 while (mInts.size() < totalLines) {
626 mInts.insertAt(mInts.size(), new int[COLUMNS_NORMAL]);
627 }
Gilles Debunne1e130b22012-03-14 17:40:42 -0700628 }
629
630 /**
Gilles Debunne33b7de852012-03-12 11:57:48 -0700631 * @hide
632 */
Gilles Debunne157aafc2012-04-19 17:21:57 -0700633 public int[] getBlockEndLines() {
634 return mBlockEndLines;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700635 }
636
637 /**
638 * @hide
639 */
640 public int[] getBlockIndices() {
641 return mBlockIndices;
642 }
643
644 /**
645 * @hide
646 */
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900647 public int getBlockIndex(int index) {
648 return mBlockIndices[index];
649 }
650
651 /**
652 * @hide
653 * @param index
654 */
655 public void setBlockIndex(int index, int blockIndex) {
656 mBlockIndices[index] = blockIndex;
657 }
658
659 /**
660 * @hide
661 */
Gilles Debunne33b7de852012-03-12 11:57:48 -0700662 public int getNumberOfBlocks() {
663 return mNumberOfBlocks;
664 }
665
Sangkyu Lee955beb22012-12-10 15:47:00 +0900666 /**
667 * @hide
668 */
669 public int getIndexFirstChangedBlock() {
670 return mIndexFirstChangedBlock;
671 }
672
673 /**
674 * @hide
675 */
676 public void setIndexFirstChangedBlock(int i) {
677 mIndexFirstChangedBlock = i;
678 }
679
Gilles Debunned6e568c2011-01-25 10:14:43 -0800680 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800681 public int getLineCount() {
682 return mInts.size() - 1;
683 }
684
Gilles Debunned6e568c2011-01-25 10:14:43 -0800685 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800686 public int getLineTop(int line) {
687 return mInts.getValue(line, TOP);
688 }
689
Gilles Debunned6e568c2011-01-25 10:14:43 -0800690 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800691 public int getLineDescent(int line) {
692 return mInts.getValue(line, DESCENT);
693 }
694
Gilles Debunned6e568c2011-01-25 10:14:43 -0800695 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800696 public int getLineStart(int line) {
697 return mInts.getValue(line, START) & START_MASK;
698 }
699
Gilles Debunned6e568c2011-01-25 10:14:43 -0800700 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800701 public boolean getLineContainsTab(int line) {
702 return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
703 }
704
Gilles Debunned6e568c2011-01-25 10:14:43 -0800705 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800706 public int getParagraphDirection(int line) {
707 return mInts.getValue(line, DIR) >> DIR_SHIFT;
708 }
709
Gilles Debunned6e568c2011-01-25 10:14:43 -0800710 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800711 public final Directions getLineDirections(int line) {
712 return mObjects.getValue(line, 0);
713 }
714
Gilles Debunned6e568c2011-01-25 10:14:43 -0800715 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800716 public int getTopPadding() {
717 return mTopPadding;
718 }
719
Gilles Debunned6e568c2011-01-25 10:14:43 -0800720 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800721 public int getBottomPadding() {
722 return mBottomPadding;
723 }
724
Raph Levien26d443a2015-03-30 14:18:32 -0700725 /**
726 * @hide
727 */
728 @Override
729 public int getHyphen(int line) {
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900730 return mInts.getValue(line, HYPHEN) & HYPHEN_MASK;
731 }
732
733 private boolean getContentMayProtrudeFromTopOrBottom(int line) {
734 return (mInts.getValue(line, MAY_PROTRUDE_FROM_TOP_OR_BOTTOM)
735 & MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK) != 0;
Raph Levien26d443a2015-03-30 14:18:32 -0700736 }
737
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800738 @Override
739 public int getEllipsizedWidth() {
740 return mEllipsizedWidth;
741 }
742
Gilles Debunne0a4db3c2011-01-14 12:12:04 -0800743 private static class ChangeWatcher implements TextWatcher, SpanWatcher {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800744 public ChangeWatcher(DynamicLayout layout) {
Gilles Debunned6e568c2011-01-25 10:14:43 -0800745 mLayout = new WeakReference<DynamicLayout>(layout);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800746 }
747
748 private void reflow(CharSequence s, int where, int before, int after) {
Gilles Debunned6e568c2011-01-25 10:14:43 -0800749 DynamicLayout ml = mLayout.get();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800750
751 if (ml != null)
752 ml.reflow(s, where, before, after);
753 else if (s instanceof Spannable)
754 ((Spannable) s).removeSpan(this);
755 }
756
Gilles Debunne0a4db3c2011-01-14 12:12:04 -0800757 public void beforeTextChanged(CharSequence s, int where, int before, int after) {
Gilles Debunne33b7de852012-03-12 11:57:48 -0700758 // Intentionally empty
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800759 }
760
Gilles Debunne0a4db3c2011-01-14 12:12:04 -0800761 public void onTextChanged(CharSequence s, int where, int before, int after) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800762 reflow(s, where, before, after);
763 }
764
765 public void afterTextChanged(Editable s) {
Gilles Debunne33b7de852012-03-12 11:57:48 -0700766 // Intentionally empty
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800767 }
768
769 public void onSpanAdded(Spannable s, Object o, int start, int end) {
770 if (o instanceof UpdateLayout)
771 reflow(s, start, end - start, end - start);
772 }
773
774 public void onSpanRemoved(Spannable s, Object o, int start, int end) {
775 if (o instanceof UpdateLayout)
776 reflow(s, start, end - start, end - start);
777 }
778
Gilles Debunne0a4db3c2011-01-14 12:12:04 -0800779 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 -0800780 if (o instanceof UpdateLayout) {
781 reflow(s, start, end - start, end - start);
782 reflow(s, nstart, nend - nstart, nend - nstart);
783 }
784 }
785
Gilles Debunned6e568c2011-01-25 10:14:43 -0800786 private WeakReference<DynamicLayout> mLayout;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800787 }
788
Gilles Debunned6e568c2011-01-25 10:14:43 -0800789 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800790 public int getEllipsisStart(int line) {
791 if (mEllipsizeAt == null) {
792 return 0;
793 }
794
795 return mInts.getValue(line, ELLIPSIS_START);
796 }
797
Gilles Debunned6e568c2011-01-25 10:14:43 -0800798 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800799 public int getEllipsisCount(int line) {
800 if (mEllipsizeAt == null) {
801 return 0;
802 }
803
804 return mInts.getValue(line, ELLIPSIS_COUNT);
805 }
806
807 private CharSequence mBase;
808 private CharSequence mDisplay;
809 private ChangeWatcher mWatcher;
810 private boolean mIncludePad;
811 private boolean mEllipsize;
812 private int mEllipsizedWidth;
813 private TextUtils.TruncateAt mEllipsizeAt;
Raph Levien39b4db72015-03-25 13:18:20 -0700814 private int mBreakStrategy;
Roozbeh Pournader95c7a132015-05-12 12:01:06 -0700815 private int mHyphenationFrequency;
Seigo Nonaka4b4730d2017-03-31 09:42:16 -0700816 private int mJustificationMode;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800817
818 private PackedIntVector mInts;
819 private PackedObjectVector<Directions> mObjects;
820
Romain Guycde6adf2012-03-15 17:04:47 -0700821 /**
Gilles Debunne33b7de852012-03-12 11:57:48 -0700822 * Value used in mBlockIndices when a block has been created or recycled and indicating that its
823 * display list needs to be re-created.
824 * @hide
825 */
826 public static final int INVALID_BLOCK_INDEX = -1;
Gilles Debunne157aafc2012-04-19 17:21:57 -0700827 // Stores the line numbers of the last line of each block (inclusive)
828 private int[] mBlockEndLines;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700829 // The indices of this block's display list in TextView's internal display list array or
830 // INVALID_BLOCK_INDEX if this block has been invalidated during an edition
831 private int[] mBlockIndices;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900832 // Set of blocks that always need to be redrawn.
833 private ArraySet<Integer> mBlocksAlwaysNeedToBeRedrawn;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700834 // Number of items actually currently being used in the above 2 arrays
835 private int mNumberOfBlocks;
Sangkyu Lee955beb22012-12-10 15:47:00 +0900836 // The first index of the blocks whose locations are changed
837 private int mIndexFirstChangedBlock;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700838
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800839 private int mTopPadding, mBottomPadding;
840
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900841 private Rect mTempRect = new Rect();
842
Raph Leviend3ab6922015-03-02 14:30:53 -0800843 private static StaticLayout sStaticLayout = null;
844 private static StaticLayout.Builder sBuilder = null;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700845
Romain Guye5ea4402011-08-01 14:01:37 -0700846 private static final Object[] sLock = new Object[0];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800847
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900848 // START, DIR, and TAB share the same entry.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800849 private static final int START = 0;
850 private static final int DIR = START;
851 private static final int TAB = START;
852 private static final int TOP = 1;
853 private static final int DESCENT = 2;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900854 // HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry.
Raph Levien26d443a2015-03-30 14:18:32 -0700855 private static final int HYPHEN = 3;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900856 private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN;
Raph Levien26d443a2015-03-30 14:18:32 -0700857 private static final int COLUMNS_NORMAL = 4;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800858
Raph Levien26d443a2015-03-30 14:18:32 -0700859 private static final int ELLIPSIS_START = 4;
860 private static final int ELLIPSIS_COUNT = 5;
861 private static final int COLUMNS_ELLIPSIZE = 6;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800862
863 private static final int START_MASK = 0x1FFFFFFF;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800864 private static final int DIR_SHIFT = 30;
865 private static final int TAB_MASK = 0x20000000;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900866 private static final int HYPHEN_MASK = 0xFF;
867 private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK = 0x100;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800868
869 private static final int ELLIPSIS_UNDEFINED = 0x80000000;
870}