blob: d6a68fb54b7c62d2ef42a0f01250514cccda8acb [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)
Siyamed Sinir442c1512017-07-24 12:18:27 -0700306 .setJustificationMode(mJustificationMode)
Siyamed Sinirfb0b2dc2017-07-26 22:08:24 -0700307 .setAddLastLineLineSpacing(!islast);
308
Siyamed Sinir442c1512017-07-24 12:18:27 -0700309 reflowed.generate(b, false /*includepad*/, true /*trackpad*/);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800310 int n = reflowed.getLineCount();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800311 // If the new layout has a blank line at the end, but it is not
312 // the very end of the buffer, then we already have a line that
313 // starts there, so disregard the blank line.
314
Gilles Debunne71afc392012-05-10 10:24:20 -0700315 if (where + after != len && reflowed.getLineStart(n - 1) == where + after)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800316 n--;
317
318 // remove affected lines from old layout
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800319 mInts.deleteAt(startline, endline - startline);
320 mObjects.deleteAt(startline, endline - startline);
321
322 // adjust offsets in layout for new height and offsets
323
324 int ht = reflowed.getLineTop(n);
325 int toppad = 0, botpad = 0;
326
327 if (mIncludePad && startline == 0) {
328 toppad = reflowed.getTopPadding();
329 mTopPadding = toppad;
330 ht -= toppad;
331 }
332 if (mIncludePad && islast) {
333 botpad = reflowed.getBottomPadding();
334 mBottomPadding = botpad;
335 ht += botpad;
336 }
337
338 mInts.adjustValuesBelow(startline, START, after - before);
339 mInts.adjustValuesBelow(startline, TOP, startv - endv + ht);
340
341 // insert new layout
342
343 int[] ints;
344
345 if (mEllipsize) {
346 ints = new int[COLUMNS_ELLIPSIZE];
347 ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
348 } else {
349 ints = new int[COLUMNS_NORMAL];
350 }
351
352 Directions[] objects = new Directions[1];
353
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800354 for (int i = 0; i < n; i++) {
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900355 final int start = reflowed.getLineStart(i);
356 ints[START] = start;
357 ints[DIR] |= reflowed.getParagraphDirection(i) << DIR_SHIFT;
358 ints[TAB] |= reflowed.getLineContainsTab(i) ? TAB_MASK : 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800359
360 int top = reflowed.getLineTop(i) + startv;
361 if (i > 0)
362 top -= toppad;
363 ints[TOP] = top;
364
365 int desc = reflowed.getLineDescent(i);
366 if (i == n - 1)
367 desc += botpad;
368
369 ints[DESCENT] = desc;
Siyamed Sinir0fa89d62017-07-24 20:46:41 -0700370 ints[EXTRA] = reflowed.getLineExtra(i);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800371 objects[0] = reflowed.getLineDirections(i);
372
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900373 final int end = (i == n - 1) ? where + after : reflowed.getLineStart(i + 1);
374 ints[HYPHEN] = reflowed.getHyphen(i) & HYPHEN_MASK;
375 ints[MAY_PROTRUDE_FROM_TOP_OR_BOTTOM] |=
376 contentMayProtrudeFromLineTopOrBottom(text, start, end) ?
377 MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK : 0;
Raph Levien26d443a2015-03-30 14:18:32 -0700378
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800379 if (mEllipsize) {
380 ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
381 ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i);
382 }
383
384 mInts.insertAt(startline + i, ints);
385 mObjects.insertAt(startline + i, objects);
386 }
387
Gilles Debunne71afc392012-05-10 10:24:20 -0700388 updateBlocks(startline, endline - 1, n);
389
Raph Leviend3ab6922015-03-02 14:30:53 -0800390 b.finish();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800391 synchronized (sLock) {
Amith Yamasanib724c342011-08-13 07:43:07 -0700392 sStaticLayout = reflowed;
Raph Leviend3ab6922015-03-02 14:30:53 -0800393 sBuilder = b;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800394 }
395 }
396
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900397 private boolean contentMayProtrudeFromLineTopOrBottom(CharSequence text, int start, int end) {
398 if (text instanceof Spanned) {
399 final Spanned spanned = (Spanned) text;
400 if (spanned.getSpans(start, end, ReplacementSpan.class).length > 0) {
401 return true;
402 }
403 }
404 // Spans other than ReplacementSpan can be ignored because line top and bottom are
405 // disjunction of all tops and bottoms, although it's not optimal.
406 final Paint paint = getPaint();
407 paint.getTextBounds(text, start, end, mTempRect);
408 final Paint.FontMetricsInt fm = paint.getFontMetricsInt();
409 return mTempRect.top < fm.top || mTempRect.bottom > fm.bottom;
410 }
411
Gilles Debunne33b7de852012-03-12 11:57:48 -0700412 /**
Gilles Debunne71afc392012-05-10 10:24:20 -0700413 * Create the initial block structure, cutting the text into blocks of at least
414 * BLOCK_MINIMUM_CHARACTER_SIZE characters, aligned on the ends of paragraphs.
415 */
416 private void createBlocks() {
417 int offset = BLOCK_MINIMUM_CHARACTER_LENGTH;
418 mNumberOfBlocks = 0;
419 final CharSequence text = mDisplay;
420
421 while (true) {
422 offset = TextUtils.indexOf(text, '\n', offset);
423 if (offset < 0) {
424 addBlockAtOffset(text.length());
425 break;
426 } else {
427 addBlockAtOffset(offset);
428 offset += BLOCK_MINIMUM_CHARACTER_LENGTH;
429 }
430 }
431
432 // mBlockIndices and mBlockEndLines should have the same length
433 mBlockIndices = new int[mBlockEndLines.length];
434 for (int i = 0; i < mBlockEndLines.length; i++) {
435 mBlockIndices[i] = INVALID_BLOCK_INDEX;
436 }
437 }
438
439 /**
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900440 * @hide
441 */
442 public ArraySet<Integer> getBlocksAlwaysNeedToBeRedrawn() {
443 return mBlocksAlwaysNeedToBeRedrawn;
444 }
445
446 private void updateAlwaysNeedsToBeRedrawn(int blockIndex) {
447 int startLine = blockIndex == 0 ? 0 : (mBlockEndLines[blockIndex - 1] + 1);
448 int endLine = mBlockEndLines[blockIndex];
449 for (int i = startLine; i <= endLine; i++) {
450 if (getContentMayProtrudeFromTopOrBottom(i)) {
451 if (mBlocksAlwaysNeedToBeRedrawn == null) {
452 mBlocksAlwaysNeedToBeRedrawn = new ArraySet<>();
453 }
454 mBlocksAlwaysNeedToBeRedrawn.add(blockIndex);
455 return;
456 }
457 }
458 if (mBlocksAlwaysNeedToBeRedrawn != null) {
459 mBlocksAlwaysNeedToBeRedrawn.remove(blockIndex);
460 }
461 }
462
463 /**
Gilles Debunne71afc392012-05-10 10:24:20 -0700464 * Create a new block, ending at the specified character offset.
465 * A block will actually be created only if has at least one line, i.e. this offset is
466 * not on the end line of the previous block.
467 */
468 private void addBlockAtOffset(int offset) {
469 final int line = getLineForOffset(offset);
Gilles Debunne71afc392012-05-10 10:24:20 -0700470 if (mBlockEndLines == null) {
471 // Initial creation of the array, no test on previous block ending line
Adam Lesinski776abc22014-03-07 11:30:59 -0500472 mBlockEndLines = ArrayUtils.newUnpaddedIntArray(1);
Gilles Debunne71afc392012-05-10 10:24:20 -0700473 mBlockEndLines[mNumberOfBlocks] = line;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900474 updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
Gilles Debunne71afc392012-05-10 10:24:20 -0700475 mNumberOfBlocks++;
476 return;
477 }
478
479 final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1];
480 if (line > previousBlockEndLine) {
Adam Lesinski776abc22014-03-07 11:30:59 -0500481 mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line);
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900482 updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
Gilles Debunne71afc392012-05-10 10:24:20 -0700483 mNumberOfBlocks++;
484 }
485 }
486
487 /**
Gilles Debunne33b7de852012-03-12 11:57:48 -0700488 * This method is called every time the layout is reflowed after an edition.
489 * It updates the internal block data structure. The text is split in blocks
490 * of contiguous lines, with at least one block for the entire text.
491 * When a range of lines is edited, new blocks (from 0 to 3 depending on the
492 * overlap structure) will replace the set of overlapping blocks.
493 * Blocks are listed in order and are represented by their ending line number.
494 * An index is associated to each block (which will be used by display lists),
495 * this class simply invalidates the index of blocks overlapping a modification.
496 *
497 * @param startLine the first line of the range of modified lines
498 * @param endLine the last line of the range, possibly equal to startLine, lower
499 * than getLineCount()
500 * @param newLineCount the number of lines that will replace the range, possibly 0
Gilles Debunne1e130b22012-03-14 17:40:42 -0700501 *
502 * @hide
Gilles Debunne33b7de852012-03-12 11:57:48 -0700503 */
Siyamed Sinired09ae12016-02-16 14:36:26 -0800504 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
505 public void updateBlocks(int startLine, int endLine, int newLineCount) {
Gilles Debunne71afc392012-05-10 10:24:20 -0700506 if (mBlockEndLines == null) {
507 createBlocks();
508 return;
509 }
510
Gilles Debunne33b7de852012-03-12 11:57:48 -0700511 int firstBlock = -1;
512 int lastBlock = -1;
513 for (int i = 0; i < mNumberOfBlocks; i++) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700514 if (mBlockEndLines[i] >= startLine) {
Gilles Debunne33b7de852012-03-12 11:57:48 -0700515 firstBlock = i;
516 break;
517 }
518 }
519 for (int i = firstBlock; i < mNumberOfBlocks; i++) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700520 if (mBlockEndLines[i] >= endLine) {
Gilles Debunne33b7de852012-03-12 11:57:48 -0700521 lastBlock = i;
522 break;
523 }
524 }
Gilles Debunne157aafc2012-04-19 17:21:57 -0700525 final int lastBlockEndLine = mBlockEndLines[lastBlock];
Gilles Debunne33b7de852012-03-12 11:57:48 -0700526
527 boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
Gilles Debunne157aafc2012-04-19 17:21:57 -0700528 mBlockEndLines[firstBlock - 1] + 1);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700529 boolean createBlock = newLineCount > 0;
Gilles Debunne157aafc2012-04-19 17:21:57 -0700530 boolean createBlockAfter = endLine < mBlockEndLines[lastBlock];
Gilles Debunne33b7de852012-03-12 11:57:48 -0700531
532 int numAddedBlocks = 0;
533 if (createBlockBefore) numAddedBlocks++;
534 if (createBlock) numAddedBlocks++;
535 if (createBlockAfter) numAddedBlocks++;
536
537 final int numRemovedBlocks = lastBlock - firstBlock + 1;
538 final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks;
539
540 if (newNumberOfBlocks == 0) {
541 // Even when text is empty, there is actually one line and hence one block
Gilles Debunne157aafc2012-04-19 17:21:57 -0700542 mBlockEndLines[0] = 0;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700543 mBlockIndices[0] = INVALID_BLOCK_INDEX;
544 mNumberOfBlocks = 1;
545 return;
546 }
547
Gilles Debunne157aafc2012-04-19 17:21:57 -0700548 if (newNumberOfBlocks > mBlockEndLines.length) {
Adam Lesinski776abc22014-03-07 11:30:59 -0500549 int[] blockEndLines = ArrayUtils.newUnpaddedIntArray(
550 Math.max(mBlockEndLines.length * 2, newNumberOfBlocks));
551 int[] blockIndices = new int[blockEndLines.length];
Gilles Debunne157aafc2012-04-19 17:21:57 -0700552 System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700553 System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock);
Gilles Debunne157aafc2012-04-19 17:21:57 -0700554 System.arraycopy(mBlockEndLines, lastBlock + 1,
555 blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700556 System.arraycopy(mBlockIndices, lastBlock + 1,
557 blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
Gilles Debunne157aafc2012-04-19 17:21:57 -0700558 mBlockEndLines = blockEndLines;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700559 mBlockIndices = blockIndices;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900560 } else if (numAddedBlocks + numRemovedBlocks != 0) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700561 System.arraycopy(mBlockEndLines, lastBlock + 1,
562 mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700563 System.arraycopy(mBlockIndices, lastBlock + 1,
564 mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
565 }
566
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900567 if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) {
568 final ArraySet<Integer> set = new ArraySet<>();
569 for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) {
570 Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i);
571 if (block > firstBlock) {
572 block += numAddedBlocks - numRemovedBlocks;
573 }
574 set.add(block);
575 }
576 mBlocksAlwaysNeedToBeRedrawn = set;
577 }
578
Gilles Debunne33b7de852012-03-12 11:57:48 -0700579 mNumberOfBlocks = newNumberOfBlocks;
Raph Levien63b3eb62013-10-03 13:48:04 -0700580 int newFirstChangedBlock;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700581 final int deltaLines = newLineCount - (endLine - startLine + 1);
Sangkyu Lee955beb22012-12-10 15:47:00 +0900582 if (deltaLines != 0) {
583 // Display list whose index is >= mIndexFirstChangedBlock is valid
584 // but it needs to update its drawing location.
Raph Levien63b3eb62013-10-03 13:48:04 -0700585 newFirstChangedBlock = firstBlock + numAddedBlocks;
586 for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) {
Sangkyu Lee955beb22012-12-10 15:47:00 +0900587 mBlockEndLines[i] += deltaLines;
588 }
589 } else {
Raph Levien63b3eb62013-10-03 13:48:04 -0700590 newFirstChangedBlock = mNumberOfBlocks;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700591 }
Raph Levien63b3eb62013-10-03 13:48:04 -0700592 mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700593
594 int blockIndex = firstBlock;
595 if (createBlockBefore) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700596 mBlockEndLines[blockIndex] = startLine - 1;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900597 updateAlwaysNeedsToBeRedrawn(blockIndex);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700598 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
599 blockIndex++;
600 }
601
602 if (createBlock) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700603 mBlockEndLines[blockIndex] = startLine + newLineCount - 1;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900604 updateAlwaysNeedsToBeRedrawn(blockIndex);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700605 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
606 blockIndex++;
607 }
608
609 if (createBlockAfter) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700610 mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900611 updateAlwaysNeedsToBeRedrawn(blockIndex);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700612 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
613 }
614 }
615
616 /**
Roozbeh Pournaderfc778692017-05-17 14:24:02 -0700617 * This method is used for test purposes only.
Gilles Debunne1e130b22012-03-14 17:40:42 -0700618 * @hide
619 */
Siyamed Sinired09ae12016-02-16 14:36:26 -0800620 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
Roozbeh Pournaderfc778692017-05-17 14:24:02 -0700621 public void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks,
622 int totalLines) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700623 mBlockEndLines = new int[blockEndLines.length];
Gilles Debunne1e130b22012-03-14 17:40:42 -0700624 mBlockIndices = new int[blockIndices.length];
Gilles Debunne157aafc2012-04-19 17:21:57 -0700625 System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length);
Gilles Debunne1e130b22012-03-14 17:40:42 -0700626 System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length);
627 mNumberOfBlocks = numberOfBlocks;
Roozbeh Pournaderfc778692017-05-17 14:24:02 -0700628 while (mInts.size() < totalLines) {
629 mInts.insertAt(mInts.size(), new int[COLUMNS_NORMAL]);
630 }
Gilles Debunne1e130b22012-03-14 17:40:42 -0700631 }
632
633 /**
Gilles Debunne33b7de852012-03-12 11:57:48 -0700634 * @hide
635 */
Gilles Debunne157aafc2012-04-19 17:21:57 -0700636 public int[] getBlockEndLines() {
637 return mBlockEndLines;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700638 }
639
640 /**
641 * @hide
642 */
643 public int[] getBlockIndices() {
644 return mBlockIndices;
645 }
646
647 /**
648 * @hide
649 */
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900650 public int getBlockIndex(int index) {
651 return mBlockIndices[index];
652 }
653
654 /**
655 * @hide
656 * @param index
657 */
658 public void setBlockIndex(int index, int blockIndex) {
659 mBlockIndices[index] = blockIndex;
660 }
661
662 /**
663 * @hide
664 */
Gilles Debunne33b7de852012-03-12 11:57:48 -0700665 public int getNumberOfBlocks() {
666 return mNumberOfBlocks;
667 }
668
Sangkyu Lee955beb22012-12-10 15:47:00 +0900669 /**
670 * @hide
671 */
672 public int getIndexFirstChangedBlock() {
673 return mIndexFirstChangedBlock;
674 }
675
676 /**
677 * @hide
678 */
679 public void setIndexFirstChangedBlock(int i) {
680 mIndexFirstChangedBlock = i;
681 }
682
Gilles Debunned6e568c2011-01-25 10:14:43 -0800683 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800684 public int getLineCount() {
685 return mInts.size() - 1;
686 }
687
Gilles Debunned6e568c2011-01-25 10:14:43 -0800688 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800689 public int getLineTop(int line) {
690 return mInts.getValue(line, TOP);
691 }
692
Gilles Debunned6e568c2011-01-25 10:14:43 -0800693 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800694 public int getLineDescent(int line) {
695 return mInts.getValue(line, DESCENT);
696 }
697
Siyamed Sinir0fa89d62017-07-24 20:46:41 -0700698 /**
699 * @hide
700 */
701 @Override
702 public int getLineExtra(int line) {
703 return mInts.getValue(line, EXTRA);
704 }
705
Gilles Debunned6e568c2011-01-25 10:14:43 -0800706 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800707 public int getLineStart(int line) {
708 return mInts.getValue(line, START) & START_MASK;
709 }
710
Gilles Debunned6e568c2011-01-25 10:14:43 -0800711 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800712 public boolean getLineContainsTab(int line) {
713 return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
714 }
715
Gilles Debunned6e568c2011-01-25 10:14:43 -0800716 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800717 public int getParagraphDirection(int line) {
718 return mInts.getValue(line, DIR) >> DIR_SHIFT;
719 }
720
Gilles Debunned6e568c2011-01-25 10:14:43 -0800721 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800722 public final Directions getLineDirections(int line) {
723 return mObjects.getValue(line, 0);
724 }
725
Gilles Debunned6e568c2011-01-25 10:14:43 -0800726 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800727 public int getTopPadding() {
728 return mTopPadding;
729 }
730
Gilles Debunned6e568c2011-01-25 10:14:43 -0800731 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800732 public int getBottomPadding() {
733 return mBottomPadding;
734 }
735
Raph Levien26d443a2015-03-30 14:18:32 -0700736 /**
737 * @hide
738 */
739 @Override
740 public int getHyphen(int line) {
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900741 return mInts.getValue(line, HYPHEN) & HYPHEN_MASK;
742 }
743
744 private boolean getContentMayProtrudeFromTopOrBottom(int line) {
745 return (mInts.getValue(line, MAY_PROTRUDE_FROM_TOP_OR_BOTTOM)
746 & MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK) != 0;
Raph Levien26d443a2015-03-30 14:18:32 -0700747 }
748
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800749 @Override
750 public int getEllipsizedWidth() {
751 return mEllipsizedWidth;
752 }
753
Gilles Debunne0a4db3c2011-01-14 12:12:04 -0800754 private static class ChangeWatcher implements TextWatcher, SpanWatcher {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800755 public ChangeWatcher(DynamicLayout layout) {
Gilles Debunned6e568c2011-01-25 10:14:43 -0800756 mLayout = new WeakReference<DynamicLayout>(layout);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800757 }
758
759 private void reflow(CharSequence s, int where, int before, int after) {
Gilles Debunned6e568c2011-01-25 10:14:43 -0800760 DynamicLayout ml = mLayout.get();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800761
762 if (ml != null)
763 ml.reflow(s, where, before, after);
764 else if (s instanceof Spannable)
765 ((Spannable) s).removeSpan(this);
766 }
767
Gilles Debunne0a4db3c2011-01-14 12:12:04 -0800768 public void beforeTextChanged(CharSequence s, int where, int before, int after) {
Gilles Debunne33b7de852012-03-12 11:57:48 -0700769 // Intentionally empty
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800770 }
771
Gilles Debunne0a4db3c2011-01-14 12:12:04 -0800772 public void onTextChanged(CharSequence s, int where, int before, int after) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800773 reflow(s, where, before, after);
774 }
775
776 public void afterTextChanged(Editable s) {
Gilles Debunne33b7de852012-03-12 11:57:48 -0700777 // Intentionally empty
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800778 }
779
780 public void onSpanAdded(Spannable s, Object o, int start, int end) {
781 if (o instanceof UpdateLayout)
782 reflow(s, start, end - start, end - start);
783 }
784
785 public void onSpanRemoved(Spannable s, Object o, int start, int end) {
786 if (o instanceof UpdateLayout)
787 reflow(s, start, end - start, end - start);
788 }
789
Gilles Debunne0a4db3c2011-01-14 12:12:04 -0800790 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 -0800791 if (o instanceof UpdateLayout) {
792 reflow(s, start, end - start, end - start);
793 reflow(s, nstart, nend - nstart, nend - nstart);
794 }
795 }
796
Gilles Debunned6e568c2011-01-25 10:14:43 -0800797 private WeakReference<DynamicLayout> mLayout;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800798 }
799
Gilles Debunned6e568c2011-01-25 10:14:43 -0800800 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800801 public int getEllipsisStart(int line) {
802 if (mEllipsizeAt == null) {
803 return 0;
804 }
805
806 return mInts.getValue(line, ELLIPSIS_START);
807 }
808
Gilles Debunned6e568c2011-01-25 10:14:43 -0800809 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800810 public int getEllipsisCount(int line) {
811 if (mEllipsizeAt == null) {
812 return 0;
813 }
814
815 return mInts.getValue(line, ELLIPSIS_COUNT);
816 }
817
818 private CharSequence mBase;
819 private CharSequence mDisplay;
820 private ChangeWatcher mWatcher;
821 private boolean mIncludePad;
822 private boolean mEllipsize;
823 private int mEllipsizedWidth;
824 private TextUtils.TruncateAt mEllipsizeAt;
Raph Levien39b4db72015-03-25 13:18:20 -0700825 private int mBreakStrategy;
Roozbeh Pournader95c7a132015-05-12 12:01:06 -0700826 private int mHyphenationFrequency;
Seigo Nonaka4b4730d2017-03-31 09:42:16 -0700827 private int mJustificationMode;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800828
829 private PackedIntVector mInts;
830 private PackedObjectVector<Directions> mObjects;
831
Romain Guycde6adf2012-03-15 17:04:47 -0700832 /**
Gilles Debunne33b7de852012-03-12 11:57:48 -0700833 * Value used in mBlockIndices when a block has been created or recycled and indicating that its
834 * display list needs to be re-created.
835 * @hide
836 */
837 public static final int INVALID_BLOCK_INDEX = -1;
Gilles Debunne157aafc2012-04-19 17:21:57 -0700838 // Stores the line numbers of the last line of each block (inclusive)
839 private int[] mBlockEndLines;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700840 // The indices of this block's display list in TextView's internal display list array or
841 // INVALID_BLOCK_INDEX if this block has been invalidated during an edition
842 private int[] mBlockIndices;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900843 // Set of blocks that always need to be redrawn.
844 private ArraySet<Integer> mBlocksAlwaysNeedToBeRedrawn;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700845 // Number of items actually currently being used in the above 2 arrays
846 private int mNumberOfBlocks;
Sangkyu Lee955beb22012-12-10 15:47:00 +0900847 // The first index of the blocks whose locations are changed
848 private int mIndexFirstChangedBlock;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700849
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800850 private int mTopPadding, mBottomPadding;
851
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900852 private Rect mTempRect = new Rect();
853
Raph Leviend3ab6922015-03-02 14:30:53 -0800854 private static StaticLayout sStaticLayout = null;
855 private static StaticLayout.Builder sBuilder = null;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700856
Romain Guye5ea4402011-08-01 14:01:37 -0700857 private static final Object[] sLock = new Object[0];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800858
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900859 // START, DIR, and TAB share the same entry.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800860 private static final int START = 0;
861 private static final int DIR = START;
862 private static final int TAB = START;
863 private static final int TOP = 1;
864 private static final int DESCENT = 2;
Siyamed Sinir0fa89d62017-07-24 20:46:41 -0700865 private static final int EXTRA = 3;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900866 // HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry.
Siyamed Sinir0fa89d62017-07-24 20:46:41 -0700867 private static final int HYPHEN = 4;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900868 private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN;
Siyamed Sinir0fa89d62017-07-24 20:46:41 -0700869 private static final int COLUMNS_NORMAL = 5;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800870
Siyamed Sinir0fa89d62017-07-24 20:46:41 -0700871 private static final int ELLIPSIS_START = 5;
872 private static final int ELLIPSIS_COUNT = 6;
873 private static final int COLUMNS_ELLIPSIZE = 7;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800874
875 private static final int START_MASK = 0x1FFFFFFF;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800876 private static final int DIR_SHIFT = 30;
877 private static final int TAB_MASK = 0x20000000;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900878 private static final int HYPHEN_MASK = 0xFF;
879 private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK = 0x100;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800880
881 private static final int ELLIPSIS_UNDEFINED = 0x80000000;
882}