1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.text;
18 
19 import android.graphics.Paint;
20 import android.text.style.UpdateLayout;
21 import android.text.style.WrapTogetherSpan;
22 
23 import com.android.internal.util.ArrayUtils;
24 import com.android.internal.util.GrowingArrayUtils;
25 
26 import java.lang.ref.WeakReference;
27 
28 /**
29  * DynamicLayout is a text layout that updates itself as the text is edited.
30  * <p>This is used by widgets to control text layout. You should not need
31  * to use this class directly unless you are implementing your own widget
32  * or custom display object, or need to call
33  * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
34  *  Canvas.drawText()} directly.</p>
35  */
36 public class DynamicLayout extends Layout
37 {
38     private static final int PRIORITY = 128;
39     private static final int BLOCK_MINIMUM_CHARACTER_LENGTH = 400;
40 
41     /**
42      * Make a layout for the specified text that will be updated as
43      * the text is changed.
44      */
DynamicLayout(CharSequence base, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad)45     public DynamicLayout(CharSequence base,
46                          TextPaint paint,
47                          int width, Alignment align,
48                          float spacingmult, float spacingadd,
49                          boolean includepad) {
50         this(base, base, paint, width, align, spacingmult, spacingadd,
51              includepad);
52     }
53 
54     /**
55      * Make a layout for the transformed text (password transformation
56      * being the primary example of a transformation)
57      * that will be updated as the base text is changed.
58      */
DynamicLayout(CharSequence base, CharSequence display, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad)59     public DynamicLayout(CharSequence base, CharSequence display,
60                          TextPaint paint,
61                          int width, Alignment align,
62                          float spacingmult, float spacingadd,
63                          boolean includepad) {
64         this(base, display, paint, width, align, spacingmult, spacingadd,
65              includepad, null, 0);
66     }
67 
68     /**
69      * Make a layout for the transformed text (password transformation
70      * being the primary example of a transformation)
71      * that will be updated as the base text is changed.
72      * If ellipsize is non-null, the Layout will ellipsize the text
73      * down to ellipsizedWidth.
74      */
DynamicLayout(CharSequence base, CharSequence display, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)75     public DynamicLayout(CharSequence base, CharSequence display,
76                          TextPaint paint,
77                          int width, Alignment align,
78                          float spacingmult, float spacingadd,
79                          boolean includepad,
80                          TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
81         this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
82                 spacingmult, spacingadd, includepad,
83                 StaticLayout.BREAK_STRATEGY_SIMPLE, StaticLayout.HYPHENATION_FREQUENCY_NONE,
84                 ellipsize, ellipsizedWidth);
85     }
86 
87     /**
88      * Make a layout for the transformed text (password transformation
89      * being the primary example of a transformation)
90      * that will be updated as the base text is changed.
91      * If ellipsize is non-null, the Layout will ellipsize the text
92      * down to ellipsizedWidth.
93      * *
94      * *@hide
95      */
DynamicLayout(CharSequence base, CharSequence display, TextPaint paint, int width, Alignment align, TextDirectionHeuristic textDir, float spacingmult, float spacingadd, boolean includepad, int breakStrategy, int hyphenationFrequency, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)96     public DynamicLayout(CharSequence base, CharSequence display,
97                          TextPaint paint,
98                          int width, Alignment align, TextDirectionHeuristic textDir,
99                          float spacingmult, float spacingadd,
100                          boolean includepad, int breakStrategy, int hyphenationFrequency,
101                          TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
102         super((ellipsize == null)
103                 ? display
104                 : (display instanceof Spanned)
105                     ? new SpannedEllipsizer(display)
106                     : new Ellipsizer(display),
107               paint, width, align, textDir, spacingmult, spacingadd);
108 
109         mBase = base;
110         mDisplay = display;
111 
112         if (ellipsize != null) {
113             mInts = new PackedIntVector(COLUMNS_ELLIPSIZE);
114             mEllipsizedWidth = ellipsizedWidth;
115             mEllipsizeAt = ellipsize;
116         } else {
117             mInts = new PackedIntVector(COLUMNS_NORMAL);
118             mEllipsizedWidth = width;
119             mEllipsizeAt = null;
120         }
121 
122         mObjects = new PackedObjectVector<Directions>(1);
123 
124         mIncludePad = includepad;
125         mBreakStrategy = breakStrategy;
126         mHyphenationFrequency = hyphenationFrequency;
127 
128         /*
129          * This is annoying, but we can't refer to the layout until
130          * superclass construction is finished, and the superclass
131          * constructor wants the reference to the display text.
132          *
133          * This will break if the superclass constructor ever actually
134          * cares about the content instead of just holding the reference.
135          */
136         if (ellipsize != null) {
137             Ellipsizer e = (Ellipsizer) getText();
138 
139             e.mLayout = this;
140             e.mWidth = ellipsizedWidth;
141             e.mMethod = ellipsize;
142             mEllipsize = true;
143         }
144 
145         // Initial state is a single line with 0 characters (0 to 0),
146         // with top at 0 and bottom at whatever is natural, and
147         // undefined ellipsis.
148 
149         int[] start;
150 
151         if (ellipsize != null) {
152             start = new int[COLUMNS_ELLIPSIZE];
153             start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
154         } else {
155             start = new int[COLUMNS_NORMAL];
156         }
157 
158         Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT };
159 
160         Paint.FontMetricsInt fm = paint.getFontMetricsInt();
161         int asc = fm.ascent;
162         int desc = fm.descent;
163 
164         start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT;
165         start[TOP] = 0;
166         start[DESCENT] = desc;
167         mInts.insertAt(0, start);
168 
169         start[TOP] = desc - asc;
170         mInts.insertAt(1, start);
171 
172         mObjects.insertAt(0, dirs);
173 
174         // Update from 0 characters to whatever the real text is
175         reflow(base, 0, 0, base.length());
176 
177         if (base instanceof Spannable) {
178             if (mWatcher == null)
179                 mWatcher = new ChangeWatcher(this);
180 
181             // Strip out any watchers for other DynamicLayouts.
182             Spannable sp = (Spannable) base;
183             ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class);
184             for (int i = 0; i < spans.length; i++)
185                 sp.removeSpan(spans[i]);
186 
187             sp.setSpan(mWatcher, 0, base.length(),
188                        Spannable.SPAN_INCLUSIVE_INCLUSIVE |
189                        (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT));
190         }
191     }
192 
reflow(CharSequence s, int where, int before, int after)193     private void reflow(CharSequence s, int where, int before, int after) {
194         if (s != mBase)
195             return;
196 
197         CharSequence text = mDisplay;
198         int len = text.length();
199 
200         // seek back to the start of the paragraph
201 
202         int find = TextUtils.lastIndexOf(text, '\n', where - 1);
203         if (find < 0)
204             find = 0;
205         else
206             find = find + 1;
207 
208         {
209             int diff = where - find;
210             before += diff;
211             after += diff;
212             where -= diff;
213         }
214 
215         // seek forward to the end of the paragraph
216 
217         int look = TextUtils.indexOf(text, '\n', where + after);
218         if (look < 0)
219             look = len;
220         else
221             look++; // we want the index after the \n
222 
223         int change = look - (where + after);
224         before += change;
225         after += change;
226 
227         // seek further out to cover anything that is forced to wrap together
228 
229         if (text instanceof Spanned) {
230             Spanned sp = (Spanned) text;
231             boolean again;
232 
233             do {
234                 again = false;
235 
236                 Object[] force = sp.getSpans(where, where + after,
237                                              WrapTogetherSpan.class);
238 
239                 for (int i = 0; i < force.length; i++) {
240                     int st = sp.getSpanStart(force[i]);
241                     int en = sp.getSpanEnd(force[i]);
242 
243                     if (st < where) {
244                         again = true;
245 
246                         int diff = where - st;
247                         before += diff;
248                         after += diff;
249                         where -= diff;
250                     }
251 
252                     if (en > where + after) {
253                         again = true;
254 
255                         int diff = en - (where + after);
256                         before += diff;
257                         after += diff;
258                     }
259                 }
260             } while (again);
261         }
262 
263         // find affected region of old layout
264 
265         int startline = getLineForOffset(where);
266         int startv = getLineTop(startline);
267 
268         int endline = getLineForOffset(where + before);
269         if (where + after == len)
270             endline = getLineCount();
271         int endv = getLineTop(endline);
272         boolean islast = (endline == getLineCount());
273 
274         // generate new layout for affected text
275 
276         StaticLayout reflowed;
277         StaticLayout.Builder b;
278 
279         synchronized (sLock) {
280             reflowed = sStaticLayout;
281             b = sBuilder;
282             sStaticLayout = null;
283             sBuilder = null;
284         }
285 
286         if (reflowed == null) {
287             reflowed = new StaticLayout(null);
288             b = StaticLayout.Builder.obtain(text, where, where + after, getPaint(), getWidth());
289         }
290 
291         b.setText(text, where, where + after)
292                 .setPaint(getPaint())
293                 .setWidth(getWidth())
294                 .setTextDirection(getTextDirectionHeuristic())
295                 .setLineSpacing(getSpacingAdd(), getSpacingMultiplier())
296                 .setEllipsizedWidth(mEllipsizedWidth)
297                 .setEllipsize(mEllipsizeAt)
298                 .setBreakStrategy(mBreakStrategy)
299                 .setHyphenationFrequency(mHyphenationFrequency);
300         reflowed.generate(b, false, true);
301         int n = reflowed.getLineCount();
302 
303         // If the new layout has a blank line at the end, but it is not
304         // the very end of the buffer, then we already have a line that
305         // starts there, so disregard the blank line.
306 
307         if (where + after != len && reflowed.getLineStart(n - 1) == where + after)
308             n--;
309 
310         // remove affected lines from old layout
311         mInts.deleteAt(startline, endline - startline);
312         mObjects.deleteAt(startline, endline - startline);
313 
314         // adjust offsets in layout for new height and offsets
315 
316         int ht = reflowed.getLineTop(n);
317         int toppad = 0, botpad = 0;
318 
319         if (mIncludePad && startline == 0) {
320             toppad = reflowed.getTopPadding();
321             mTopPadding = toppad;
322             ht -= toppad;
323         }
324         if (mIncludePad && islast) {
325             botpad = reflowed.getBottomPadding();
326             mBottomPadding = botpad;
327             ht += botpad;
328         }
329 
330         mInts.adjustValuesBelow(startline, START, after - before);
331         mInts.adjustValuesBelow(startline, TOP, startv - endv + ht);
332 
333         // insert new layout
334 
335         int[] ints;
336 
337         if (mEllipsize) {
338             ints = new int[COLUMNS_ELLIPSIZE];
339             ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
340         } else {
341             ints = new int[COLUMNS_NORMAL];
342         }
343 
344         Directions[] objects = new Directions[1];
345 
346         for (int i = 0; i < n; i++) {
347             ints[START] = reflowed.getLineStart(i) |
348                           (reflowed.getParagraphDirection(i) << DIR_SHIFT) |
349                           (reflowed.getLineContainsTab(i) ? TAB_MASK : 0);
350 
351             int top = reflowed.getLineTop(i) + startv;
352             if (i > 0)
353                 top -= toppad;
354             ints[TOP] = top;
355 
356             int desc = reflowed.getLineDescent(i);
357             if (i == n - 1)
358                 desc += botpad;
359 
360             ints[DESCENT] = desc;
361             objects[0] = reflowed.getLineDirections(i);
362 
363             ints[HYPHEN] = reflowed.getHyphen(i);
364 
365             if (mEllipsize) {
366                 ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
367                 ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i);
368             }
369 
370             mInts.insertAt(startline + i, ints);
371             mObjects.insertAt(startline + i, objects);
372         }
373 
374         updateBlocks(startline, endline - 1, n);
375 
376         b.finish();
377         synchronized (sLock) {
378             sStaticLayout = reflowed;
379             sBuilder = b;
380         }
381     }
382 
383     /**
384      * Create the initial block structure, cutting the text into blocks of at least
385      * BLOCK_MINIMUM_CHARACTER_SIZE characters, aligned on the ends of paragraphs.
386      */
createBlocks()387     private void createBlocks() {
388         int offset = BLOCK_MINIMUM_CHARACTER_LENGTH;
389         mNumberOfBlocks = 0;
390         final CharSequence text = mDisplay;
391 
392         while (true) {
393             offset = TextUtils.indexOf(text, '\n', offset);
394             if (offset < 0) {
395                 addBlockAtOffset(text.length());
396                 break;
397             } else {
398                 addBlockAtOffset(offset);
399                 offset += BLOCK_MINIMUM_CHARACTER_LENGTH;
400             }
401         }
402 
403         // mBlockIndices and mBlockEndLines should have the same length
404         mBlockIndices = new int[mBlockEndLines.length];
405         for (int i = 0; i < mBlockEndLines.length; i++) {
406             mBlockIndices[i] = INVALID_BLOCK_INDEX;
407         }
408     }
409 
410     /**
411      * Create a new block, ending at the specified character offset.
412      * A block will actually be created only if has at least one line, i.e. this offset is
413      * not on the end line of the previous block.
414      */
addBlockAtOffset(int offset)415     private void addBlockAtOffset(int offset) {
416         final int line = getLineForOffset(offset);
417 
418         if (mBlockEndLines == null) {
419             // Initial creation of the array, no test on previous block ending line
420             mBlockEndLines = ArrayUtils.newUnpaddedIntArray(1);
421             mBlockEndLines[mNumberOfBlocks] = line;
422             mNumberOfBlocks++;
423             return;
424         }
425 
426         final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1];
427         if (line > previousBlockEndLine) {
428             mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line);
429             mNumberOfBlocks++;
430         }
431     }
432 
433     /**
434      * This method is called every time the layout is reflowed after an edition.
435      * It updates the internal block data structure. The text is split in blocks
436      * of contiguous lines, with at least one block for the entire text.
437      * When a range of lines is edited, new blocks (from 0 to 3 depending on the
438      * overlap structure) will replace the set of overlapping blocks.
439      * Blocks are listed in order and are represented by their ending line number.
440      * An index is associated to each block (which will be used by display lists),
441      * this class simply invalidates the index of blocks overlapping a modification.
442      *
443      * This method is package private and not private so that it can be tested.
444      *
445      * @param startLine the first line of the range of modified lines
446      * @param endLine the last line of the range, possibly equal to startLine, lower
447      * than getLineCount()
448      * @param newLineCount the number of lines that will replace the range, possibly 0
449      *
450      * @hide
451      */
updateBlocks(int startLine, int endLine, int newLineCount)452     void updateBlocks(int startLine, int endLine, int newLineCount) {
453         if (mBlockEndLines == null) {
454             createBlocks();
455             return;
456         }
457 
458         int firstBlock = -1;
459         int lastBlock = -1;
460         for (int i = 0; i < mNumberOfBlocks; i++) {
461             if (mBlockEndLines[i] >= startLine) {
462                 firstBlock = i;
463                 break;
464             }
465         }
466         for (int i = firstBlock; i < mNumberOfBlocks; i++) {
467             if (mBlockEndLines[i] >= endLine) {
468                 lastBlock = i;
469                 break;
470             }
471         }
472         final int lastBlockEndLine = mBlockEndLines[lastBlock];
473 
474         boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
475                 mBlockEndLines[firstBlock - 1] + 1);
476         boolean createBlock = newLineCount > 0;
477         boolean createBlockAfter = endLine < mBlockEndLines[lastBlock];
478 
479         int numAddedBlocks = 0;
480         if (createBlockBefore) numAddedBlocks++;
481         if (createBlock) numAddedBlocks++;
482         if (createBlockAfter) numAddedBlocks++;
483 
484         final int numRemovedBlocks = lastBlock - firstBlock + 1;
485         final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks;
486 
487         if (newNumberOfBlocks == 0) {
488             // Even when text is empty, there is actually one line and hence one block
489             mBlockEndLines[0] = 0;
490             mBlockIndices[0] = INVALID_BLOCK_INDEX;
491             mNumberOfBlocks = 1;
492             return;
493         }
494 
495         if (newNumberOfBlocks > mBlockEndLines.length) {
496             int[] blockEndLines = ArrayUtils.newUnpaddedIntArray(
497                     Math.max(mBlockEndLines.length * 2, newNumberOfBlocks));
498             int[] blockIndices = new int[blockEndLines.length];
499             System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock);
500             System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock);
501             System.arraycopy(mBlockEndLines, lastBlock + 1,
502                     blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
503             System.arraycopy(mBlockIndices, lastBlock + 1,
504                     blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
505             mBlockEndLines = blockEndLines;
506             mBlockIndices = blockIndices;
507         } else {
508             System.arraycopy(mBlockEndLines, lastBlock + 1,
509                     mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
510             System.arraycopy(mBlockIndices, lastBlock + 1,
511                     mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
512         }
513 
514         mNumberOfBlocks = newNumberOfBlocks;
515         int newFirstChangedBlock;
516         final int deltaLines = newLineCount - (endLine - startLine + 1);
517         if (deltaLines != 0) {
518             // Display list whose index is >= mIndexFirstChangedBlock is valid
519             // but it needs to update its drawing location.
520             newFirstChangedBlock = firstBlock + numAddedBlocks;
521             for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) {
522                 mBlockEndLines[i] += deltaLines;
523             }
524         } else {
525             newFirstChangedBlock = mNumberOfBlocks;
526         }
527         mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock);
528 
529         int blockIndex = firstBlock;
530         if (createBlockBefore) {
531             mBlockEndLines[blockIndex] = startLine - 1;
532             mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
533             blockIndex++;
534         }
535 
536         if (createBlock) {
537             mBlockEndLines[blockIndex] = startLine + newLineCount - 1;
538             mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
539             blockIndex++;
540         }
541 
542         if (createBlockAfter) {
543             mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines;
544             mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
545         }
546     }
547 
548     /**
549      * This package private method is used for test purposes only
550      * @hide
551      */
setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks)552     void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks) {
553         mBlockEndLines = new int[blockEndLines.length];
554         mBlockIndices = new int[blockIndices.length];
555         System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length);
556         System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length);
557         mNumberOfBlocks = numberOfBlocks;
558     }
559 
560     /**
561      * @hide
562      */
getBlockEndLines()563     public int[] getBlockEndLines() {
564         return mBlockEndLines;
565     }
566 
567     /**
568      * @hide
569      */
getBlockIndices()570     public int[] getBlockIndices() {
571         return mBlockIndices;
572     }
573 
574     /**
575      * @hide
576      */
getNumberOfBlocks()577     public int getNumberOfBlocks() {
578         return mNumberOfBlocks;
579     }
580 
581     /**
582      * @hide
583      */
getIndexFirstChangedBlock()584     public int getIndexFirstChangedBlock() {
585         return mIndexFirstChangedBlock;
586     }
587 
588     /**
589      * @hide
590      */
setIndexFirstChangedBlock(int i)591     public void setIndexFirstChangedBlock(int i) {
592         mIndexFirstChangedBlock = i;
593     }
594 
595     @Override
getLineCount()596     public int getLineCount() {
597         return mInts.size() - 1;
598     }
599 
600     @Override
getLineTop(int line)601     public int getLineTop(int line) {
602         return mInts.getValue(line, TOP);
603     }
604 
605     @Override
getLineDescent(int line)606     public int getLineDescent(int line) {
607         return mInts.getValue(line, DESCENT);
608     }
609 
610     @Override
getLineStart(int line)611     public int getLineStart(int line) {
612         return mInts.getValue(line, START) & START_MASK;
613     }
614 
615     @Override
getLineContainsTab(int line)616     public boolean getLineContainsTab(int line) {
617         return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
618     }
619 
620     @Override
getParagraphDirection(int line)621     public int getParagraphDirection(int line) {
622         return mInts.getValue(line, DIR) >> DIR_SHIFT;
623     }
624 
625     @Override
getLineDirections(int line)626     public final Directions getLineDirections(int line) {
627         return mObjects.getValue(line, 0);
628     }
629 
630     @Override
getTopPadding()631     public int getTopPadding() {
632         return mTopPadding;
633     }
634 
635     @Override
getBottomPadding()636     public int getBottomPadding() {
637         return mBottomPadding;
638     }
639 
640     /**
641      * @hide
642      */
643     @Override
getHyphen(int line)644     public int getHyphen(int line) {
645         return mInts.getValue(line, HYPHEN);
646     }
647 
648     @Override
getEllipsizedWidth()649     public int getEllipsizedWidth() {
650         return mEllipsizedWidth;
651     }
652 
653     private static class ChangeWatcher implements TextWatcher, SpanWatcher {
ChangeWatcher(DynamicLayout layout)654         public ChangeWatcher(DynamicLayout layout) {
655             mLayout = new WeakReference<DynamicLayout>(layout);
656         }
657 
reflow(CharSequence s, int where, int before, int after)658         private void reflow(CharSequence s, int where, int before, int after) {
659             DynamicLayout ml = mLayout.get();
660 
661             if (ml != null)
662                 ml.reflow(s, where, before, after);
663             else if (s instanceof Spannable)
664                 ((Spannable) s).removeSpan(this);
665         }
666 
beforeTextChanged(CharSequence s, int where, int before, int after)667         public void beforeTextChanged(CharSequence s, int where, int before, int after) {
668             // Intentionally empty
669         }
670 
onTextChanged(CharSequence s, int where, int before, int after)671         public void onTextChanged(CharSequence s, int where, int before, int after) {
672             reflow(s, where, before, after);
673         }
674 
afterTextChanged(Editable s)675         public void afterTextChanged(Editable s) {
676             // Intentionally empty
677         }
678 
onSpanAdded(Spannable s, Object o, int start, int end)679         public void onSpanAdded(Spannable s, Object o, int start, int end) {
680             if (o instanceof UpdateLayout)
681                 reflow(s, start, end - start, end - start);
682         }
683 
onSpanRemoved(Spannable s, Object o, int start, int end)684         public void onSpanRemoved(Spannable s, Object o, int start, int end) {
685             if (o instanceof UpdateLayout)
686                 reflow(s, start, end - start, end - start);
687         }
688 
onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend)689         public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) {
690             if (o instanceof UpdateLayout) {
691                 reflow(s, start, end - start, end - start);
692                 reflow(s, nstart, nend - nstart, nend - nstart);
693             }
694         }
695 
696         private WeakReference<DynamicLayout> mLayout;
697     }
698 
699     @Override
getEllipsisStart(int line)700     public int getEllipsisStart(int line) {
701         if (mEllipsizeAt == null) {
702             return 0;
703         }
704 
705         return mInts.getValue(line, ELLIPSIS_START);
706     }
707 
708     @Override
getEllipsisCount(int line)709     public int getEllipsisCount(int line) {
710         if (mEllipsizeAt == null) {
711             return 0;
712         }
713 
714         return mInts.getValue(line, ELLIPSIS_COUNT);
715     }
716 
717     private CharSequence mBase;
718     private CharSequence mDisplay;
719     private ChangeWatcher mWatcher;
720     private boolean mIncludePad;
721     private boolean mEllipsize;
722     private int mEllipsizedWidth;
723     private TextUtils.TruncateAt mEllipsizeAt;
724     private int mBreakStrategy;
725     private int mHyphenationFrequency;
726 
727     private PackedIntVector mInts;
728     private PackedObjectVector<Directions> mObjects;
729 
730     /**
731      * Value used in mBlockIndices when a block has been created or recycled and indicating that its
732      * display list needs to be re-created.
733      * @hide
734      */
735     public static final int INVALID_BLOCK_INDEX = -1;
736     // Stores the line numbers of the last line of each block (inclusive)
737     private int[] mBlockEndLines;
738     // The indices of this block's display list in TextView's internal display list array or
739     // INVALID_BLOCK_INDEX if this block has been invalidated during an edition
740     private int[] mBlockIndices;
741     // Number of items actually currently being used in the above 2 arrays
742     private int mNumberOfBlocks;
743     // The first index of the blocks whose locations are changed
744     private int mIndexFirstChangedBlock;
745 
746     private int mTopPadding, mBottomPadding;
747 
748     private static StaticLayout sStaticLayout = null;
749     private static StaticLayout.Builder sBuilder = null;
750 
751     private static final Object[] sLock = new Object[0];
752 
753     private static final int START = 0;
754     private static final int DIR = START;
755     private static final int TAB = START;
756     private static final int TOP = 1;
757     private static final int DESCENT = 2;
758     private static final int HYPHEN = 3;
759     private static final int COLUMNS_NORMAL = 4;
760 
761     private static final int ELLIPSIS_START = 4;
762     private static final int ELLIPSIS_COUNT = 5;
763     private static final int COLUMNS_ELLIPSIZE = 6;
764 
765     private static final int START_MASK = 0x1FFFFFFF;
766     private static final int DIR_SHIFT  = 30;
767     private static final int TAB_MASK   = 0x20000000;
768 
769     private static final int ELLIPSIS_UNDEFINED = 0x80000000;
770 }
771