1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.text;
18 
19 import android.graphics.Paint;
20 import android.graphics.Rect;
21 import android.text.style.ReplacementSpan;
22 import android.text.style.UpdateLayout;
23 import android.text.style.WrapTogetherSpan;
24 import android.util.ArraySet;
25 
26 import com.android.internal.annotations.VisibleForTesting;
27 import com.android.internal.util.ArrayUtils;
28 import com.android.internal.util.GrowingArrayUtils;
29 
30 import 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  */
40 public class DynamicLayout extends Layout
41 {
42     private static final int PRIORITY = 128;
43     private static final int BLOCK_MINIMUM_CHARACTER_LENGTH = 400;
44 
45     /**
46      * Make a layout for the specified text that will be updated as
47      * the text is changed.
48      */
DynamicLayout(CharSequence base, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad)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      */
DynamicLayout(CharSequence base, CharSequence display, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad)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      */
DynamicLayout(CharSequence base, CharSequence display, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)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) {
85         this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
86                 spacingmult, spacingadd, includepad,
87                 StaticLayout.BREAK_STRATEGY_SIMPLE, StaticLayout.HYPHENATION_FREQUENCY_NONE,
88                 Layout.JUSTIFICATION_MODE_NONE, ellipsize, ellipsizedWidth);
89     }
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      */
DynamicLayout(CharSequence base, CharSequence display, TextPaint paint, int width, Alignment align, TextDirectionHeuristic textDir, float spacingmult, float spacingadd, boolean includepad, int breakStrategy, int hyphenationFrequency, int justificationMode, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)100     public DynamicLayout(CharSequence base, CharSequence display,
101                          TextPaint paint,
102                          int width, Alignment align, TextDirectionHeuristic textDir,
103                          float spacingmult, float spacingadd,
104                          boolean includepad, int breakStrategy, int hyphenationFrequency,
105                          int justificationMode, TextUtils.TruncateAt ellipsize,
106                          int ellipsizedWidth) {
107         super((ellipsize == null)
108                 ? display
109                 : (display instanceof Spanned)
110                     ? new SpannedEllipsizer(display)
111                     : new Ellipsizer(display),
112               paint, width, align, textDir, spacingmult, spacingadd);
113 
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;
124             mEllipsizeAt = null;
125         }
126 
127         mObjects = new PackedObjectVector<Directions>(1);
128 
129         mIncludePad = includepad;
130         mBreakStrategy = breakStrategy;
131         mJustificationMode = justificationMode;
132         mHyphenationFrequency = hyphenationFrequency;
133 
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
181         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 
reflow(CharSequence s, int where, int before, int after)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;
283         StaticLayout.Builder b;
284 
285         synchronized (sLock) {
286             reflowed = sStaticLayout;
287             b = sBuilder;
288             sStaticLayout = null;
289             sBuilder = null;
290         }
291 
292         if (reflowed == null) {
293             reflowed = new StaticLayout(null);
294             b = StaticLayout.Builder.obtain(text, where, where + after, getPaint(), getWidth());
295         }
296 
297         b.setText(text, where, where + after)
298                 .setPaint(getPaint())
299                 .setWidth(getWidth())
300                 .setTextDirection(getTextDirectionHeuristic())
301                 .setLineSpacing(getSpacingAdd(), getSpacingMultiplier())
302                 .setEllipsizedWidth(mEllipsizedWidth)
303                 .setEllipsize(mEllipsizeAt)
304                 .setBreakStrategy(mBreakStrategy)
305                 .setHyphenationFrequency(mHyphenationFrequency)
306                 .setJustificationMode(mJustificationMode);
307         reflowed.generate(b, false, true);
308         int n = reflowed.getLineCount();
309         // 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 
313         if (where + after != len && reflowed.getLineStart(n - 1) == where + after)
314             n--;
315 
316         // remove affected lines from old layout
317         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 
352         for (int i = 0; i < n; i++) {
353             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;
357 
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 
370             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;
375 
376             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 
385         updateBlocks(startline, endline - 1, n);
386 
387         b.finish();
388         synchronized (sLock) {
389             sStaticLayout = reflowed;
390             sBuilder = b;
391         }
392     }
393 
contentMayProtrudeFromLineTopOrBottom(CharSequence text, int start, int end)394     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 
409     /**
410      * 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      */
createBlocks()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     /**
437      * @hide
438      */
getBlocksAlwaysNeedToBeRedrawn()439     public ArraySet<Integer> getBlocksAlwaysNeedToBeRedrawn() {
440         return mBlocksAlwaysNeedToBeRedrawn;
441     }
442 
updateAlwaysNeedsToBeRedrawn(int blockIndex)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     /**
461      * 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      */
addBlockAtOffset(int offset)465     private void addBlockAtOffset(int offset) {
466         final int line = getLineForOffset(offset);
467         if (mBlockEndLines == null) {
468             // Initial creation of the array, no test on previous block ending line
469             mBlockEndLines = ArrayUtils.newUnpaddedIntArray(1);
470             mBlockEndLines[mNumberOfBlocks] = line;
471             updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
472             mNumberOfBlocks++;
473             return;
474         }
475 
476         final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1];
477         if (line > previousBlockEndLine) {
478             mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line);
479             updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
480             mNumberOfBlocks++;
481         }
482     }
483 
484     /**
485      * 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      * This method is package private and not private so that it can be tested.
495      *
496      * @param startLine the first line of the range of modified lines
497      * @param endLine the last line of the range, possibly equal to startLine, lower
498      * than getLineCount()
499      * @param newLineCount the number of lines that will replace the range, possibly 0
500      *
501      * @hide
502      */
503     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
updateBlocks(int startLine, int endLine, int newLineCount)504     public void updateBlocks(int startLine, int endLine, int newLineCount) {
505         if (mBlockEndLines == null) {
506             createBlocks();
507             return;
508         }
509 
510         int firstBlock = -1;
511         int lastBlock = -1;
512         for (int i = 0; i < mNumberOfBlocks; i++) {
513             if (mBlockEndLines[i] >= startLine) {
514                 firstBlock = i;
515                 break;
516             }
517         }
518         for (int i = firstBlock; i < mNumberOfBlocks; i++) {
519             if (mBlockEndLines[i] >= endLine) {
520                 lastBlock = i;
521                 break;
522             }
523         }
524         final int lastBlockEndLine = mBlockEndLines[lastBlock];
525 
526         boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
527                 mBlockEndLines[firstBlock - 1] + 1);
528         boolean createBlock = newLineCount > 0;
529         boolean createBlockAfter = endLine < mBlockEndLines[lastBlock];
530 
531         int numAddedBlocks = 0;
532         if (createBlockBefore) numAddedBlocks++;
533         if (createBlock) numAddedBlocks++;
534         if (createBlockAfter) numAddedBlocks++;
535 
536         final int numRemovedBlocks = lastBlock - firstBlock + 1;
537         final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks;
538 
539         if (newNumberOfBlocks == 0) {
540             // Even when text is empty, there is actually one line and hence one block
541             mBlockEndLines[0] = 0;
542             mBlockIndices[0] = INVALID_BLOCK_INDEX;
543             mNumberOfBlocks = 1;
544             return;
545         }
546 
547         if (newNumberOfBlocks > mBlockEndLines.length) {
548             int[] blockEndLines = ArrayUtils.newUnpaddedIntArray(
549                     Math.max(mBlockEndLines.length * 2, newNumberOfBlocks));
550             int[] blockIndices = new int[blockEndLines.length];
551             System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock);
552             System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock);
553             System.arraycopy(mBlockEndLines, lastBlock + 1,
554                     blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
555             System.arraycopy(mBlockIndices, lastBlock + 1,
556                     blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
557             mBlockEndLines = blockEndLines;
558             mBlockIndices = blockIndices;
559         } else if (numAddedBlocks + numRemovedBlocks != 0) {
560             System.arraycopy(mBlockEndLines, lastBlock + 1,
561                     mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
562             System.arraycopy(mBlockIndices, lastBlock + 1,
563                     mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
564         }
565 
566         if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) {
567             final ArraySet<Integer> set = new ArraySet<>();
568             for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) {
569                 Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i);
570                 if (block > firstBlock) {
571                     block += numAddedBlocks - numRemovedBlocks;
572                 }
573                 set.add(block);
574             }
575             mBlocksAlwaysNeedToBeRedrawn = set;
576         }
577 
578         mNumberOfBlocks = newNumberOfBlocks;
579         int newFirstChangedBlock;
580         final int deltaLines = newLineCount - (endLine - startLine + 1);
581         if (deltaLines != 0) {
582             // Display list whose index is >= mIndexFirstChangedBlock is valid
583             // but it needs to update its drawing location.
584             newFirstChangedBlock = firstBlock + numAddedBlocks;
585             for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) {
586                 mBlockEndLines[i] += deltaLines;
587             }
588         } else {
589             newFirstChangedBlock = mNumberOfBlocks;
590         }
591         mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock);
592 
593         int blockIndex = firstBlock;
594         if (createBlockBefore) {
595             mBlockEndLines[blockIndex] = startLine - 1;
596             updateAlwaysNeedsToBeRedrawn(blockIndex);
597             mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
598             blockIndex++;
599         }
600 
601         if (createBlock) {
602             mBlockEndLines[blockIndex] = startLine + newLineCount - 1;
603             updateAlwaysNeedsToBeRedrawn(blockIndex);
604             mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
605             blockIndex++;
606         }
607 
608         if (createBlockAfter) {
609             mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines;
610             updateAlwaysNeedsToBeRedrawn(blockIndex);
611             mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
612         }
613     }
614 
615     /**
616      * This package private method is used for test purposes only
617      * @hide
618      */
619     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks)620     public void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks) {
621         mBlockEndLines = new int[blockEndLines.length];
622         mBlockIndices = new int[blockIndices.length];
623         System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length);
624         System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length);
625         mNumberOfBlocks = numberOfBlocks;
626     }
627 
628     /**
629      * @hide
630      */
getBlockEndLines()631     public int[] getBlockEndLines() {
632         return mBlockEndLines;
633     }
634 
635     /**
636      * @hide
637      */
getBlockIndices()638     public int[] getBlockIndices() {
639         return mBlockIndices;
640     }
641 
642     /**
643      * @hide
644      */
getBlockIndex(int index)645     public int getBlockIndex(int index) {
646         return mBlockIndices[index];
647     }
648 
649     /**
650      * @hide
651      * @param index
652      */
setBlockIndex(int index, int blockIndex)653     public void setBlockIndex(int index, int blockIndex) {
654         mBlockIndices[index] = blockIndex;
655     }
656 
657     /**
658      * @hide
659      */
getNumberOfBlocks()660     public int getNumberOfBlocks() {
661         return mNumberOfBlocks;
662     }
663 
664     /**
665      * @hide
666      */
getIndexFirstChangedBlock()667     public int getIndexFirstChangedBlock() {
668         return mIndexFirstChangedBlock;
669     }
670 
671     /**
672      * @hide
673      */
setIndexFirstChangedBlock(int i)674     public void setIndexFirstChangedBlock(int i) {
675         mIndexFirstChangedBlock = i;
676     }
677 
678     @Override
getLineCount()679     public int getLineCount() {
680         return mInts.size() - 1;
681     }
682 
683     @Override
getLineTop(int line)684     public int getLineTop(int line) {
685         return mInts.getValue(line, TOP);
686     }
687 
688     @Override
getLineDescent(int line)689     public int getLineDescent(int line) {
690         return mInts.getValue(line, DESCENT);
691     }
692 
693     @Override
getLineStart(int line)694     public int getLineStart(int line) {
695         return mInts.getValue(line, START) & START_MASK;
696     }
697 
698     @Override
getLineContainsTab(int line)699     public boolean getLineContainsTab(int line) {
700         return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
701     }
702 
703     @Override
getParagraphDirection(int line)704     public int getParagraphDirection(int line) {
705         return mInts.getValue(line, DIR) >> DIR_SHIFT;
706     }
707 
708     @Override
getLineDirections(int line)709     public final Directions getLineDirections(int line) {
710         return mObjects.getValue(line, 0);
711     }
712 
713     @Override
getTopPadding()714     public int getTopPadding() {
715         return mTopPadding;
716     }
717 
718     @Override
getBottomPadding()719     public int getBottomPadding() {
720         return mBottomPadding;
721     }
722 
723     /**
724      * @hide
725      */
726     @Override
getHyphen(int line)727     public int getHyphen(int line) {
728         return mInts.getValue(line, HYPHEN) & HYPHEN_MASK;
729     }
730 
getContentMayProtrudeFromTopOrBottom(int line)731     private boolean getContentMayProtrudeFromTopOrBottom(int line) {
732         return (mInts.getValue(line, MAY_PROTRUDE_FROM_TOP_OR_BOTTOM)
733                 & MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK) != 0;
734     }
735 
736     @Override
getEllipsizedWidth()737     public int getEllipsizedWidth() {
738         return mEllipsizedWidth;
739     }
740 
741     private static class ChangeWatcher implements TextWatcher, SpanWatcher {
ChangeWatcher(DynamicLayout layout)742         public ChangeWatcher(DynamicLayout layout) {
743             mLayout = new WeakReference<DynamicLayout>(layout);
744         }
745 
reflow(CharSequence s, int where, int before, int after)746         private void reflow(CharSequence s, int where, int before, int after) {
747             DynamicLayout ml = mLayout.get();
748 
749             if (ml != null)
750                 ml.reflow(s, where, before, after);
751             else if (s instanceof Spannable)
752                 ((Spannable) s).removeSpan(this);
753         }
754 
beforeTextChanged(CharSequence s, int where, int before, int after)755         public void beforeTextChanged(CharSequence s, int where, int before, int after) {
756             // Intentionally empty
757         }
758 
onTextChanged(CharSequence s, int where, int before, int after)759         public void onTextChanged(CharSequence s, int where, int before, int after) {
760             reflow(s, where, before, after);
761         }
762 
afterTextChanged(Editable s)763         public void afterTextChanged(Editable s) {
764             // Intentionally empty
765         }
766 
onSpanAdded(Spannable s, Object o, int start, int end)767         public void onSpanAdded(Spannable s, Object o, int start, int end) {
768             if (o instanceof UpdateLayout)
769                 reflow(s, start, end - start, end - start);
770         }
771 
onSpanRemoved(Spannable s, Object o, int start, int end)772         public void onSpanRemoved(Spannable s, Object o, int start, int end) {
773             if (o instanceof UpdateLayout)
774                 reflow(s, start, end - start, end - start);
775         }
776 
onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend)777         public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) {
778             if (o instanceof UpdateLayout) {
779                 reflow(s, start, end - start, end - start);
780                 reflow(s, nstart, nend - nstart, nend - nstart);
781             }
782         }
783 
784         private WeakReference<DynamicLayout> mLayout;
785     }
786 
787     @Override
getEllipsisStart(int line)788     public int getEllipsisStart(int line) {
789         if (mEllipsizeAt == null) {
790             return 0;
791         }
792 
793         return mInts.getValue(line, ELLIPSIS_START);
794     }
795 
796     @Override
getEllipsisCount(int line)797     public int getEllipsisCount(int line) {
798         if (mEllipsizeAt == null) {
799             return 0;
800         }
801 
802         return mInts.getValue(line, ELLIPSIS_COUNT);
803     }
804 
805     private CharSequence mBase;
806     private CharSequence mDisplay;
807     private ChangeWatcher mWatcher;
808     private boolean mIncludePad;
809     private boolean mEllipsize;
810     private int mEllipsizedWidth;
811     private TextUtils.TruncateAt mEllipsizeAt;
812     private int mBreakStrategy;
813     private int mHyphenationFrequency;
814     private int mJustificationMode;
815 
816     private PackedIntVector mInts;
817     private PackedObjectVector<Directions> mObjects;
818 
819     /**
820      * Value used in mBlockIndices when a block has been created or recycled and indicating that its
821      * display list needs to be re-created.
822      * @hide
823      */
824     public static final int INVALID_BLOCK_INDEX = -1;
825     // Stores the line numbers of the last line of each block (inclusive)
826     private int[] mBlockEndLines;
827     // The indices of this block's display list in TextView's internal display list array or
828     // INVALID_BLOCK_INDEX if this block has been invalidated during an edition
829     private int[] mBlockIndices;
830     // Set of blocks that always need to be redrawn.
831     private ArraySet<Integer> mBlocksAlwaysNeedToBeRedrawn;
832     // Number of items actually currently being used in the above 2 arrays
833     private int mNumberOfBlocks;
834     // The first index of the blocks whose locations are changed
835     private int mIndexFirstChangedBlock;
836 
837     private int mTopPadding, mBottomPadding;
838 
839     private Rect mTempRect = new Rect();
840 
841     private static StaticLayout sStaticLayout = null;
842     private static StaticLayout.Builder sBuilder = null;
843 
844     private static final Object[] sLock = new Object[0];
845 
846     // START, DIR, and TAB share the same entry.
847     private static final int START = 0;
848     private static final int DIR = START;
849     private static final int TAB = START;
850     private static final int TOP = 1;
851     private static final int DESCENT = 2;
852     // HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry.
853     private static final int HYPHEN = 3;
854     private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN;
855     private static final int COLUMNS_NORMAL = 4;
856 
857     private static final int ELLIPSIS_START = 4;
858     private static final int ELLIPSIS_COUNT = 5;
859     private static final int COLUMNS_ELLIPSIZE = 6;
860 
861     private static final int START_MASK = 0x1FFFFFFF;
862     private static final int DIR_SHIFT  = 30;
863     private static final int TAB_MASK   = 0x20000000;
864     private static final int HYPHEN_MASK = 0xFF;
865     private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK = 0x100;
866 
867     private static final int ELLIPSIS_UNDEFINED = 0x80000000;
868 }
869