• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      * @param startLine the first line of the range of modified lines
495      * @param endLine the last line of the range, possibly equal to startLine, lower
496      * than getLineCount()
497      * @param newLineCount the number of lines that will replace the range, possibly 0
498      *
499      * @hide
500      */
501     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
updateBlocks(int startLine, int endLine, int newLineCount)502     public void updateBlocks(int startLine, int endLine, int newLineCount) {
503         if (mBlockEndLines == null) {
504             createBlocks();
505             return;
506         }
507 
508         int firstBlock = -1;
509         int lastBlock = -1;
510         for (int i = 0; i < mNumberOfBlocks; i++) {
511             if (mBlockEndLines[i] >= startLine) {
512                 firstBlock = i;
513                 break;
514             }
515         }
516         for (int i = firstBlock; i < mNumberOfBlocks; i++) {
517             if (mBlockEndLines[i] >= endLine) {
518                 lastBlock = i;
519                 break;
520             }
521         }
522         final int lastBlockEndLine = mBlockEndLines[lastBlock];
523 
524         boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
525                 mBlockEndLines[firstBlock - 1] + 1);
526         boolean createBlock = newLineCount > 0;
527         boolean createBlockAfter = endLine < mBlockEndLines[lastBlock];
528 
529         int numAddedBlocks = 0;
530         if (createBlockBefore) numAddedBlocks++;
531         if (createBlock) numAddedBlocks++;
532         if (createBlockAfter) numAddedBlocks++;
533 
534         final int numRemovedBlocks = lastBlock - firstBlock + 1;
535         final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks;
536 
537         if (newNumberOfBlocks == 0) {
538             // Even when text is empty, there is actually one line and hence one block
539             mBlockEndLines[0] = 0;
540             mBlockIndices[0] = INVALID_BLOCK_INDEX;
541             mNumberOfBlocks = 1;
542             return;
543         }
544 
545         if (newNumberOfBlocks > mBlockEndLines.length) {
546             int[] blockEndLines = ArrayUtils.newUnpaddedIntArray(
547                     Math.max(mBlockEndLines.length * 2, newNumberOfBlocks));
548             int[] blockIndices = new int[blockEndLines.length];
549             System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock);
550             System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock);
551             System.arraycopy(mBlockEndLines, lastBlock + 1,
552                     blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
553             System.arraycopy(mBlockIndices, lastBlock + 1,
554                     blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
555             mBlockEndLines = blockEndLines;
556             mBlockIndices = blockIndices;
557         } else if (numAddedBlocks + numRemovedBlocks != 0) {
558             System.arraycopy(mBlockEndLines, lastBlock + 1,
559                     mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
560             System.arraycopy(mBlockIndices, lastBlock + 1,
561                     mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
562         }
563 
564         if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) {
565             final ArraySet<Integer> set = new ArraySet<>();
566             for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) {
567                 Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i);
568                 if (block > firstBlock) {
569                     block += numAddedBlocks - numRemovedBlocks;
570                 }
571                 set.add(block);
572             }
573             mBlocksAlwaysNeedToBeRedrawn = set;
574         }
575 
576         mNumberOfBlocks = newNumberOfBlocks;
577         int newFirstChangedBlock;
578         final int deltaLines = newLineCount - (endLine - startLine + 1);
579         if (deltaLines != 0) {
580             // Display list whose index is >= mIndexFirstChangedBlock is valid
581             // but it needs to update its drawing location.
582             newFirstChangedBlock = firstBlock + numAddedBlocks;
583             for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) {
584                 mBlockEndLines[i] += deltaLines;
585             }
586         } else {
587             newFirstChangedBlock = mNumberOfBlocks;
588         }
589         mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock);
590 
591         int blockIndex = firstBlock;
592         if (createBlockBefore) {
593             mBlockEndLines[blockIndex] = startLine - 1;
594             updateAlwaysNeedsToBeRedrawn(blockIndex);
595             mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
596             blockIndex++;
597         }
598 
599         if (createBlock) {
600             mBlockEndLines[blockIndex] = startLine + newLineCount - 1;
601             updateAlwaysNeedsToBeRedrawn(blockIndex);
602             mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
603             blockIndex++;
604         }
605 
606         if (createBlockAfter) {
607             mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines;
608             updateAlwaysNeedsToBeRedrawn(blockIndex);
609             mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
610         }
611     }
612 
613     /**
614      * This method is used for test purposes only.
615      * @hide
616      */
617     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks, int totalLines)618     public void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks,
619             int totalLines) {
620         mBlockEndLines = new int[blockEndLines.length];
621         mBlockIndices = new int[blockIndices.length];
622         System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length);
623         System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length);
624         mNumberOfBlocks = numberOfBlocks;
625         while (mInts.size() < totalLines) {
626             mInts.insertAt(mInts.size(), new int[COLUMNS_NORMAL]);
627         }
628     }
629 
630     /**
631      * @hide
632      */
getBlockEndLines()633     public int[] getBlockEndLines() {
634         return mBlockEndLines;
635     }
636 
637     /**
638      * @hide
639      */
getBlockIndices()640     public int[] getBlockIndices() {
641         return mBlockIndices;
642     }
643 
644     /**
645      * @hide
646      */
getBlockIndex(int index)647     public int getBlockIndex(int index) {
648         return mBlockIndices[index];
649     }
650 
651     /**
652      * @hide
653      * @param index
654      */
setBlockIndex(int index, int blockIndex)655     public void setBlockIndex(int index, int blockIndex) {
656         mBlockIndices[index] = blockIndex;
657     }
658 
659     /**
660      * @hide
661      */
getNumberOfBlocks()662     public int getNumberOfBlocks() {
663         return mNumberOfBlocks;
664     }
665 
666     /**
667      * @hide
668      */
getIndexFirstChangedBlock()669     public int getIndexFirstChangedBlock() {
670         return mIndexFirstChangedBlock;
671     }
672 
673     /**
674      * @hide
675      */
setIndexFirstChangedBlock(int i)676     public void setIndexFirstChangedBlock(int i) {
677         mIndexFirstChangedBlock = i;
678     }
679 
680     @Override
getLineCount()681     public int getLineCount() {
682         return mInts.size() - 1;
683     }
684 
685     @Override
getLineTop(int line)686     public int getLineTop(int line) {
687         return mInts.getValue(line, TOP);
688     }
689 
690     @Override
getLineDescent(int line)691     public int getLineDescent(int line) {
692         return mInts.getValue(line, DESCENT);
693     }
694 
695     @Override
getLineStart(int line)696     public int getLineStart(int line) {
697         return mInts.getValue(line, START) & START_MASK;
698     }
699 
700     @Override
getLineContainsTab(int line)701     public boolean getLineContainsTab(int line) {
702         return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
703     }
704 
705     @Override
getParagraphDirection(int line)706     public int getParagraphDirection(int line) {
707         return mInts.getValue(line, DIR) >> DIR_SHIFT;
708     }
709 
710     @Override
getLineDirections(int line)711     public final Directions getLineDirections(int line) {
712         return mObjects.getValue(line, 0);
713     }
714 
715     @Override
getTopPadding()716     public int getTopPadding() {
717         return mTopPadding;
718     }
719 
720     @Override
getBottomPadding()721     public int getBottomPadding() {
722         return mBottomPadding;
723     }
724 
725     /**
726      * @hide
727      */
728     @Override
getHyphen(int line)729     public int getHyphen(int line) {
730         return mInts.getValue(line, HYPHEN) & HYPHEN_MASK;
731     }
732 
getContentMayProtrudeFromTopOrBottom(int line)733     private boolean getContentMayProtrudeFromTopOrBottom(int line) {
734         return (mInts.getValue(line, MAY_PROTRUDE_FROM_TOP_OR_BOTTOM)
735                 & MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK) != 0;
736     }
737 
738     @Override
getEllipsizedWidth()739     public int getEllipsizedWidth() {
740         return mEllipsizedWidth;
741     }
742 
743     private static class ChangeWatcher implements TextWatcher, SpanWatcher {
ChangeWatcher(DynamicLayout layout)744         public ChangeWatcher(DynamicLayout layout) {
745             mLayout = new WeakReference<DynamicLayout>(layout);
746         }
747 
reflow(CharSequence s, int where, int before, int after)748         private void reflow(CharSequence s, int where, int before, int after) {
749             DynamicLayout ml = mLayout.get();
750 
751             if (ml != null)
752                 ml.reflow(s, where, before, after);
753             else if (s instanceof Spannable)
754                 ((Spannable) s).removeSpan(this);
755         }
756 
beforeTextChanged(CharSequence s, int where, int before, int after)757         public void beforeTextChanged(CharSequence s, int where, int before, int after) {
758             // Intentionally empty
759         }
760 
onTextChanged(CharSequence s, int where, int before, int after)761         public void onTextChanged(CharSequence s, int where, int before, int after) {
762             reflow(s, where, before, after);
763         }
764 
afterTextChanged(Editable s)765         public void afterTextChanged(Editable s) {
766             // Intentionally empty
767         }
768 
onSpanAdded(Spannable s, Object o, int start, int end)769         public void onSpanAdded(Spannable s, Object o, int start, int end) {
770             if (o instanceof UpdateLayout)
771                 reflow(s, start, end - start, end - start);
772         }
773 
onSpanRemoved(Spannable s, Object o, int start, int end)774         public void onSpanRemoved(Spannable s, Object o, int start, int end) {
775             if (o instanceof UpdateLayout)
776                 reflow(s, start, end - start, end - start);
777         }
778 
onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend)779         public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) {
780             if (o instanceof UpdateLayout) {
781                 reflow(s, start, end - start, end - start);
782                 reflow(s, nstart, nend - nstart, nend - nstart);
783             }
784         }
785 
786         private WeakReference<DynamicLayout> mLayout;
787     }
788 
789     @Override
getEllipsisStart(int line)790     public int getEllipsisStart(int line) {
791         if (mEllipsizeAt == null) {
792             return 0;
793         }
794 
795         return mInts.getValue(line, ELLIPSIS_START);
796     }
797 
798     @Override
getEllipsisCount(int line)799     public int getEllipsisCount(int line) {
800         if (mEllipsizeAt == null) {
801             return 0;
802         }
803 
804         return mInts.getValue(line, ELLIPSIS_COUNT);
805     }
806 
807     private CharSequence mBase;
808     private CharSequence mDisplay;
809     private ChangeWatcher mWatcher;
810     private boolean mIncludePad;
811     private boolean mEllipsize;
812     private int mEllipsizedWidth;
813     private TextUtils.TruncateAt mEllipsizeAt;
814     private int mBreakStrategy;
815     private int mHyphenationFrequency;
816     private int mJustificationMode;
817 
818     private PackedIntVector mInts;
819     private PackedObjectVector<Directions> mObjects;
820 
821     /**
822      * Value used in mBlockIndices when a block has been created or recycled and indicating that its
823      * display list needs to be re-created.
824      * @hide
825      */
826     public static final int INVALID_BLOCK_INDEX = -1;
827     // Stores the line numbers of the last line of each block (inclusive)
828     private int[] mBlockEndLines;
829     // The indices of this block's display list in TextView's internal display list array or
830     // INVALID_BLOCK_INDEX if this block has been invalidated during an edition
831     private int[] mBlockIndices;
832     // Set of blocks that always need to be redrawn.
833     private ArraySet<Integer> mBlocksAlwaysNeedToBeRedrawn;
834     // Number of items actually currently being used in the above 2 arrays
835     private int mNumberOfBlocks;
836     // The first index of the blocks whose locations are changed
837     private int mIndexFirstChangedBlock;
838 
839     private int mTopPadding, mBottomPadding;
840 
841     private Rect mTempRect = new Rect();
842 
843     private static StaticLayout sStaticLayout = null;
844     private static StaticLayout.Builder sBuilder = null;
845 
846     private static final Object[] sLock = new Object[0];
847 
848     // START, DIR, and TAB share the same entry.
849     private static final int START = 0;
850     private static final int DIR = START;
851     private static final int TAB = START;
852     private static final int TOP = 1;
853     private static final int DESCENT = 2;
854     // HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry.
855     private static final int HYPHEN = 3;
856     private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN;
857     private static final int COLUMNS_NORMAL = 4;
858 
859     private static final int ELLIPSIS_START = 4;
860     private static final int ELLIPSIS_COUNT = 5;
861     private static final int COLUMNS_ELLIPSIZE = 6;
862 
863     private static final int START_MASK = 0x1FFFFFFF;
864     private static final int DIR_SHIFT  = 30;
865     private static final int TAB_MASK   = 0x20000000;
866     private static final int HYPHEN_MASK = 0xFF;
867     private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK = 0x100;
868 
869     private static final int ELLIPSIS_UNDEFINED = 0x80000000;
870 }
871