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