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.annotation.FloatRange;
20 import android.annotation.IntRange;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.compat.annotation.UnsupportedAppUsage;
24 import android.graphics.Paint;
25 import android.graphics.Rect;
26 import android.os.Build;
27 import android.text.style.ReplacementSpan;
28 import android.text.style.UpdateLayout;
29 import android.text.style.WrapTogetherSpan;
30 import android.util.ArraySet;
31 import android.util.Pools.SynchronizedPool;
32 
33 import com.android.internal.annotations.VisibleForTesting;
34 import com.android.internal.util.ArrayUtils;
35 import com.android.internal.util.GrowingArrayUtils;
36 
37 import java.lang.ref.WeakReference;
38 
39 /**
40  * DynamicLayout is a text layout that updates itself as the text is edited.
41  * <p>This is used by widgets to control text layout. You should not need
42  * to use this class directly unless you are implementing your own widget
43  * or custom display object, or need to call
44  * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
45  *  Canvas.drawText()} directly.</p>
46  */
47 public class DynamicLayout extends Layout {
48     private static final int PRIORITY = 128;
49     private static final int BLOCK_MINIMUM_CHARACTER_LENGTH = 400;
50 
51     /**
52      * Builder for dynamic layouts. The builder is the preferred pattern for constructing
53      * DynamicLayout objects and should be preferred over the constructors, particularly to access
54      * newer features. To build a dynamic layout, first call {@link #obtain} with the required
55      * arguments (base, paint, and width), then call setters for optional parameters, and finally
56      * {@link #build} to build the DynamicLayout object. Parameters not explicitly set will get
57      * default values.
58      */
59     public static final class Builder {
Builder()60         private Builder() {
61         }
62 
63         /**
64          * Obtain a builder for constructing DynamicLayout objects.
65          */
66         @NonNull
obtain(@onNull CharSequence base, @NonNull TextPaint paint, @IntRange(from = 0) int width)67         public static Builder obtain(@NonNull CharSequence base, @NonNull TextPaint paint,
68                 @IntRange(from = 0) int width) {
69             Builder b = sPool.acquire();
70             if (b == null) {
71                 b = new Builder();
72             }
73 
74             // set default initial values
75             b.mBase = base;
76             b.mDisplay = base;
77             b.mPaint = paint;
78             b.mWidth = width;
79             b.mAlignment = Alignment.ALIGN_NORMAL;
80             b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
81             b.mSpacingMult = DEFAULT_LINESPACING_MULTIPLIER;
82             b.mSpacingAdd = DEFAULT_LINESPACING_ADDITION;
83             b.mIncludePad = true;
84             b.mFallbackLineSpacing = false;
85             b.mEllipsizedWidth = width;
86             b.mEllipsize = null;
87             b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
88             b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
89             b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
90             return b;
91         }
92 
93         /**
94          * This method should be called after the layout is finished getting constructed and the
95          * builder needs to be cleaned up and returned to the pool.
96          */
recycle(@onNull Builder b)97         private static void recycle(@NonNull Builder b) {
98             b.mBase = null;
99             b.mDisplay = null;
100             b.mPaint = null;
101             sPool.release(b);
102         }
103 
104         /**
105          * Set the transformed text (password transformation being the primary example of a
106          * transformation) that will be updated as the base text is changed. The default is the
107          * 'base' text passed to the builder's constructor.
108          *
109          * @param display the transformed text
110          * @return this builder, useful for chaining
111          */
112         @NonNull
setDisplayText(@onNull CharSequence display)113         public Builder setDisplayText(@NonNull CharSequence display) {
114             mDisplay = display;
115             return this;
116         }
117 
118         /**
119          * Set the alignment. The default is {@link Layout.Alignment#ALIGN_NORMAL}.
120          *
121          * @param alignment Alignment for the resulting {@link DynamicLayout}
122          * @return this builder, useful for chaining
123          */
124         @NonNull
setAlignment(@onNull Alignment alignment)125         public Builder setAlignment(@NonNull Alignment alignment) {
126             mAlignment = alignment;
127             return this;
128         }
129 
130         /**
131          * Set the text direction heuristic. The text direction heuristic is used to resolve text
132          * direction per-paragraph based on the input text. The default is
133          * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}.
134          *
135          * @param textDir text direction heuristic for resolving bidi behavior.
136          * @return this builder, useful for chaining
137          */
138         @NonNull
setTextDirection(@onNull TextDirectionHeuristic textDir)139         public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) {
140             mTextDir = textDir;
141             return this;
142         }
143 
144         /**
145          * Set line spacing parameters. Each line will have its line spacing multiplied by
146          * {@code spacingMult} and then increased by {@code spacingAdd}. The default is 0.0 for
147          * {@code spacingAdd} and 1.0 for {@code spacingMult}.
148          *
149          * @param spacingAdd the amount of line spacing addition
150          * @param spacingMult the line spacing multiplier
151          * @return this builder, useful for chaining
152          * @see android.widget.TextView#setLineSpacing
153          */
154         @NonNull
setLineSpacing(float spacingAdd, @FloatRange(from = 0.0) float spacingMult)155         public Builder setLineSpacing(float spacingAdd, @FloatRange(from = 0.0) float spacingMult) {
156             mSpacingAdd = spacingAdd;
157             mSpacingMult = spacingMult;
158             return this;
159         }
160 
161         /**
162          * Set whether to include extra space beyond font ascent and descent (which is needed to
163          * avoid clipping in some languages, such as Arabic and Kannada). The default is
164          * {@code true}.
165          *
166          * @param includePad whether to include padding
167          * @return this builder, useful for chaining
168          * @see android.widget.TextView#setIncludeFontPadding
169          */
170         @NonNull
setIncludePad(boolean includePad)171         public Builder setIncludePad(boolean includePad) {
172             mIncludePad = includePad;
173             return this;
174         }
175 
176         /**
177          * Set whether to respect the ascent and descent of the fallback fonts that are used in
178          * displaying the text (which is needed to avoid text from consecutive lines running into
179          * each other). If set, fallback fonts that end up getting used can increase the ascent
180          * and descent of the lines that they are used on.
181          *
182          * <p>For backward compatibility reasons, the default is {@code false}, but setting this to
183          * true is strongly recommended. It is required to be true if text could be in languages
184          * like Burmese or Tibetan where text is typically much taller or deeper than Latin text.
185          *
186          * @param useLineSpacingFromFallbacks whether to expand linespacing based on fallback fonts
187          * @return this builder, useful for chaining
188          */
189         @NonNull
setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks)190         public Builder setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks) {
191             mFallbackLineSpacing = useLineSpacingFromFallbacks;
192             return this;
193         }
194 
195         /**
196          * Set the width as used for ellipsizing purposes, if it differs from the normal layout
197          * width. The default is the {@code width} passed to {@link #obtain}.
198          *
199          * @param ellipsizedWidth width used for ellipsizing, in pixels
200          * @return this builder, useful for chaining
201          * @see android.widget.TextView#setEllipsize
202          */
203         @NonNull
setEllipsizedWidth(@ntRangefrom = 0) int ellipsizedWidth)204         public Builder setEllipsizedWidth(@IntRange(from = 0) int ellipsizedWidth) {
205             mEllipsizedWidth = ellipsizedWidth;
206             return this;
207         }
208 
209         /**
210          * Set ellipsizing on the layout. Causes words that are longer than the view is wide, or
211          * exceeding the number of lines (see #setMaxLines) in the case of
212          * {@link android.text.TextUtils.TruncateAt#END} or
213          * {@link android.text.TextUtils.TruncateAt#MARQUEE}, to be ellipsized instead of broken.
214          * The default is {@code null}, indicating no ellipsis is to be applied.
215          *
216          * @param ellipsize type of ellipsis behavior
217          * @return this builder, useful for chaining
218          * @see android.widget.TextView#setEllipsize
219          */
setEllipsize(@ullable TextUtils.TruncateAt ellipsize)220         public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) {
221             mEllipsize = ellipsize;
222             return this;
223         }
224 
225         /**
226          * Set break strategy, useful for selecting high quality or balanced paragraph layout
227          * options. The default is {@link Layout#BREAK_STRATEGY_SIMPLE}.
228          *
229          * @param breakStrategy break strategy for paragraph layout
230          * @return this builder, useful for chaining
231          * @see android.widget.TextView#setBreakStrategy
232          */
233         @NonNull
setBreakStrategy(@reakStrategy int breakStrategy)234         public Builder setBreakStrategy(@BreakStrategy int breakStrategy) {
235             mBreakStrategy = breakStrategy;
236             return this;
237         }
238 
239         /**
240          * Set hyphenation frequency, to control the amount of automatic hyphenation used. The
241          * possible values are defined in {@link Layout}, by constants named with the pattern
242          * {@code HYPHENATION_FREQUENCY_*}. The default is
243          * {@link Layout#HYPHENATION_FREQUENCY_NONE}.
244          *
245          * @param hyphenationFrequency hyphenation frequency for the paragraph
246          * @return this builder, useful for chaining
247          * @see android.widget.TextView#setHyphenationFrequency
248          */
249         @NonNull
setHyphenationFrequency(@yphenationFrequency int hyphenationFrequency)250         public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) {
251             mHyphenationFrequency = hyphenationFrequency;
252             return this;
253         }
254 
255         /**
256          * Set paragraph justification mode. The default value is
257          * {@link Layout#JUSTIFICATION_MODE_NONE}. If the last line is too short for justification,
258          * the last line will be displayed with the alignment set by {@link #setAlignment}.
259          *
260          * @param justificationMode justification mode for the paragraph.
261          * @return this builder, useful for chaining.
262          */
263         @NonNull
setJustificationMode(@ustificationMode int justificationMode)264         public Builder setJustificationMode(@JustificationMode int justificationMode) {
265             mJustificationMode = justificationMode;
266             return this;
267         }
268 
269         /**
270          * Build the {@link DynamicLayout} after options have been set.
271          *
272          * <p>Note: the builder object must not be reused in any way after calling this method.
273          * Setting parameters after calling this method, or calling it a second time on the same
274          * builder object, will likely lead to unexpected results.
275          *
276          * @return the newly constructed {@link DynamicLayout} object
277          */
278         @NonNull
build()279         public DynamicLayout build() {
280             final DynamicLayout result = new DynamicLayout(this);
281             Builder.recycle(this);
282             return result;
283         }
284 
285         private CharSequence mBase;
286         private CharSequence mDisplay;
287         private TextPaint mPaint;
288         private int mWidth;
289         private Alignment mAlignment;
290         private TextDirectionHeuristic mTextDir;
291         private float mSpacingMult;
292         private float mSpacingAdd;
293         private boolean mIncludePad;
294         private boolean mFallbackLineSpacing;
295         private int mBreakStrategy;
296         private int mHyphenationFrequency;
297         private int mJustificationMode;
298         private TextUtils.TruncateAt mEllipsize;
299         private int mEllipsizedWidth;
300 
301         private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
302 
303         private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<>(3);
304     }
305 
306     /**
307      * @deprecated Use {@link Builder} instead.
308      */
309     @Deprecated
DynamicLayout(@onNull CharSequence base, @NonNull TextPaint paint, @IntRange(from = 0) int width, @NonNull Alignment align, @FloatRange(from = 0.0) float spacingmult, float spacingadd, boolean includepad)310     public DynamicLayout(@NonNull CharSequence base,
311                          @NonNull TextPaint paint,
312                          @IntRange(from = 0) int width, @NonNull Alignment align,
313                          @FloatRange(from = 0.0) float spacingmult, float spacingadd,
314                          boolean includepad) {
315         this(base, base, paint, width, align, spacingmult, spacingadd,
316              includepad);
317     }
318 
319     /**
320      * @deprecated Use {@link Builder} instead.
321      */
322     @Deprecated
DynamicLayout(@onNull CharSequence base, @NonNull CharSequence display, @NonNull TextPaint paint, @IntRange(from = 0) int width, @NonNull Alignment align, @FloatRange(from = 0.0) float spacingmult, float spacingadd, boolean includepad)323     public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display,
324                          @NonNull TextPaint paint,
325                          @IntRange(from = 0) int width, @NonNull Alignment align,
326                          @FloatRange(from = 0.0) float spacingmult, float spacingadd,
327                          boolean includepad) {
328         this(base, display, paint, width, align, spacingmult, spacingadd,
329              includepad, null, 0);
330     }
331 
332     /**
333      * @deprecated Use {@link Builder} instead.
334      */
335     @Deprecated
DynamicLayout(@onNull CharSequence base, @NonNull CharSequence display, @NonNull TextPaint paint, @IntRange(from = 0) int width, @NonNull Alignment align, @FloatRange(from = 0.0) float spacingmult, float spacingadd, boolean includepad, @Nullable TextUtils.TruncateAt ellipsize, @IntRange(from = 0) int ellipsizedWidth)336     public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display,
337                          @NonNull TextPaint paint,
338                          @IntRange(from = 0) int width, @NonNull Alignment align,
339                          @FloatRange(from = 0.0) float spacingmult, float spacingadd,
340                          boolean includepad,
341                          @Nullable TextUtils.TruncateAt ellipsize,
342                          @IntRange(from = 0) int ellipsizedWidth) {
343         this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
344                 spacingmult, spacingadd, includepad,
345                 Layout.BREAK_STRATEGY_SIMPLE, Layout.HYPHENATION_FREQUENCY_NONE,
346                 Layout.JUSTIFICATION_MODE_NONE, ellipsize, ellipsizedWidth);
347     }
348 
349     /**
350      * Make a layout for the transformed text (password transformation being the primary example of
351      * a transformation) that will be updated as the base text is changed. If ellipsize is non-null,
352      * the Layout will ellipsize the text down to ellipsizedWidth.
353      *
354      * @hide
355      * @deprecated Use {@link Builder} instead.
356      */
357     @Deprecated
358     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
DynamicLayout(@onNull CharSequence base, @NonNull CharSequence display, @NonNull TextPaint paint, @IntRange(from = 0) int width, @NonNull Alignment align, @NonNull TextDirectionHeuristic textDir, @FloatRange(from = 0.0) float spacingmult, float spacingadd, boolean includepad, @BreakStrategy int breakStrategy, @HyphenationFrequency int hyphenationFrequency, @JustificationMode int justificationMode, @Nullable TextUtils.TruncateAt ellipsize, @IntRange(from = 0) int ellipsizedWidth)359     public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display,
360                          @NonNull TextPaint paint,
361                          @IntRange(from = 0) int width,
362                          @NonNull Alignment align, @NonNull TextDirectionHeuristic textDir,
363                          @FloatRange(from = 0.0) float spacingmult, float spacingadd,
364                          boolean includepad, @BreakStrategy int breakStrategy,
365                          @HyphenationFrequency int hyphenationFrequency,
366                          @JustificationMode int justificationMode,
367                          @Nullable TextUtils.TruncateAt ellipsize,
368                          @IntRange(from = 0) int ellipsizedWidth) {
369         super(createEllipsizer(ellipsize, display),
370               paint, width, align, textDir, spacingmult, spacingadd);
371 
372         final Builder b = Builder.obtain(base, paint, width)
373                 .setAlignment(align)
374                 .setTextDirection(textDir)
375                 .setLineSpacing(spacingadd, spacingmult)
376                 .setEllipsizedWidth(ellipsizedWidth)
377                 .setEllipsize(ellipsize);
378         mDisplay = display;
379         mIncludePad = includepad;
380         mBreakStrategy = breakStrategy;
381         mJustificationMode = justificationMode;
382         mHyphenationFrequency = hyphenationFrequency;
383 
384         generate(b);
385 
386         Builder.recycle(b);
387     }
388 
DynamicLayout(@onNull Builder b)389     private DynamicLayout(@NonNull Builder b) {
390         super(createEllipsizer(b.mEllipsize, b.mDisplay),
391                 b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd);
392 
393         mDisplay = b.mDisplay;
394         mIncludePad = b.mIncludePad;
395         mBreakStrategy = b.mBreakStrategy;
396         mJustificationMode = b.mJustificationMode;
397         mHyphenationFrequency = b.mHyphenationFrequency;
398 
399         generate(b);
400     }
401 
402     @NonNull
createEllipsizer(@ullable TextUtils.TruncateAt ellipsize, @NonNull CharSequence display)403     private static CharSequence createEllipsizer(@Nullable TextUtils.TruncateAt ellipsize,
404             @NonNull CharSequence display) {
405         if (ellipsize == null) {
406             return display;
407         } else if (display instanceof Spanned) {
408             return new SpannedEllipsizer(display);
409         } else {
410             return new Ellipsizer(display);
411         }
412     }
413 
generate(@onNull Builder b)414     private void generate(@NonNull Builder b) {
415         mBase = b.mBase;
416         mFallbackLineSpacing = b.mFallbackLineSpacing;
417         if (b.mEllipsize != null) {
418             mInts = new PackedIntVector(COLUMNS_ELLIPSIZE);
419             mEllipsizedWidth = b.mEllipsizedWidth;
420             mEllipsizeAt = b.mEllipsize;
421 
422             /*
423              * This is annoying, but we can't refer to the layout until superclass construction is
424              * finished, and the superclass constructor wants the reference to the display text.
425              *
426              * In other words, the two Ellipsizer classes in Layout.java need a
427              * (Dynamic|Static)Layout as a parameter to do their calculations, but the Ellipsizers
428              * also need to be the input to the superclass's constructor (Layout). In order to go
429              * around the circular dependency, we construct the Ellipsizer with only one of the
430              * parameters, the text (in createEllipsizer). And we fill in the rest of the needed
431              * information (layout, width, and method) later, here.
432              *
433              * This will break if the superclass constructor ever actually cares about the content
434              * instead of just holding the reference.
435              */
436             final Ellipsizer e = (Ellipsizer) getText();
437             e.mLayout = this;
438             e.mWidth = b.mEllipsizedWidth;
439             e.mMethod = b.mEllipsize;
440             mEllipsize = true;
441         } else {
442             mInts = new PackedIntVector(COLUMNS_NORMAL);
443             mEllipsizedWidth = b.mWidth;
444             mEllipsizeAt = null;
445         }
446 
447         mObjects = new PackedObjectVector<>(1);
448 
449         // Initial state is a single line with 0 characters (0 to 0), with top at 0 and bottom at
450         // whatever is natural, and undefined ellipsis.
451 
452         int[] start;
453 
454         if (b.mEllipsize != null) {
455             start = new int[COLUMNS_ELLIPSIZE];
456             start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
457         } else {
458             start = new int[COLUMNS_NORMAL];
459         }
460 
461         final Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT };
462 
463         final Paint.FontMetricsInt fm = b.mFontMetricsInt;
464         b.mPaint.getFontMetricsInt(fm);
465         final int asc = fm.ascent;
466         final int desc = fm.descent;
467 
468         start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT;
469         start[TOP] = 0;
470         start[DESCENT] = desc;
471         mInts.insertAt(0, start);
472 
473         start[TOP] = desc - asc;
474         mInts.insertAt(1, start);
475 
476         mObjects.insertAt(0, dirs);
477 
478         final int baseLength = mBase.length();
479         // Update from 0 characters to whatever the real text is
480         reflow(mBase, 0, 0, baseLength);
481 
482         if (mBase instanceof Spannable) {
483             if (mWatcher == null)
484                 mWatcher = new ChangeWatcher(this);
485 
486             // Strip out any watchers for other DynamicLayouts.
487             final Spannable sp = (Spannable) mBase;
488             final ChangeWatcher[] spans = sp.getSpans(0, baseLength, ChangeWatcher.class);
489             for (int i = 0; i < spans.length; i++) {
490                 sp.removeSpan(spans[i]);
491             }
492 
493             sp.setSpan(mWatcher, 0, baseLength,
494                        Spannable.SPAN_INCLUSIVE_INCLUSIVE |
495                        (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT));
496         }
497     }
498 
499     /** @hide */
500     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
reflow(CharSequence s, int where, int before, int after)501     public void reflow(CharSequence s, int where, int before, int after) {
502         if (s != mBase)
503             return;
504 
505         CharSequence text = mDisplay;
506         int len = text.length();
507 
508         // seek back to the start of the paragraph
509 
510         int find = TextUtils.lastIndexOf(text, '\n', where - 1);
511         if (find < 0)
512             find = 0;
513         else
514             find = find + 1;
515 
516         {
517             int diff = where - find;
518             before += diff;
519             after += diff;
520             where -= diff;
521         }
522 
523         // seek forward to the end of the paragraph
524 
525         int look = TextUtils.indexOf(text, '\n', where + after);
526         if (look < 0)
527             look = len;
528         else
529             look++; // we want the index after the \n
530 
531         int change = look - (where + after);
532         before += change;
533         after += change;
534 
535         // seek further out to cover anything that is forced to wrap together
536 
537         if (text instanceof Spanned) {
538             Spanned sp = (Spanned) text;
539             boolean again;
540 
541             do {
542                 again = false;
543 
544                 Object[] force = sp.getSpans(where, where + after,
545                                              WrapTogetherSpan.class);
546 
547                 for (int i = 0; i < force.length; i++) {
548                     int st = sp.getSpanStart(force[i]);
549                     int en = sp.getSpanEnd(force[i]);
550 
551                     if (st < where) {
552                         again = true;
553 
554                         int diff = where - st;
555                         before += diff;
556                         after += diff;
557                         where -= diff;
558                     }
559 
560                     if (en > where + after) {
561                         again = true;
562 
563                         int diff = en - (where + after);
564                         before += diff;
565                         after += diff;
566                     }
567                 }
568             } while (again);
569         }
570 
571         // find affected region of old layout
572 
573         int startline = getLineForOffset(where);
574         int startv = getLineTop(startline);
575 
576         int endline = getLineForOffset(where + before);
577         if (where + after == len)
578             endline = getLineCount();
579         int endv = getLineTop(endline);
580         boolean islast = (endline == getLineCount());
581 
582         // generate new layout for affected text
583 
584         StaticLayout reflowed;
585         StaticLayout.Builder b;
586 
587         synchronized (sLock) {
588             reflowed = sStaticLayout;
589             b = sBuilder;
590             sStaticLayout = null;
591             sBuilder = null;
592         }
593 
594         if (reflowed == null) {
595             reflowed = new StaticLayout(null);
596             b = StaticLayout.Builder.obtain(text, where, where + after, getPaint(), getWidth());
597         }
598 
599         b.setText(text, where, where + after)
600                 .setPaint(getPaint())
601                 .setWidth(getWidth())
602                 .setTextDirection(getTextDirectionHeuristic())
603                 .setLineSpacing(getSpacingAdd(), getSpacingMultiplier())
604                 .setUseLineSpacingFromFallbacks(mFallbackLineSpacing)
605                 .setEllipsizedWidth(mEllipsizedWidth)
606                 .setEllipsize(mEllipsizeAt)
607                 .setBreakStrategy(mBreakStrategy)
608                 .setHyphenationFrequency(mHyphenationFrequency)
609                 .setJustificationMode(mJustificationMode)
610                 .setAddLastLineLineSpacing(!islast);
611 
612         reflowed.generate(b, false /*includepad*/, true /*trackpad*/);
613         int n = reflowed.getLineCount();
614         // If the new layout has a blank line at the end, but it is not
615         // the very end of the buffer, then we already have a line that
616         // starts there, so disregard the blank line.
617 
618         if (where + after != len && reflowed.getLineStart(n - 1) == where + after)
619             n--;
620 
621         // remove affected lines from old layout
622         mInts.deleteAt(startline, endline - startline);
623         mObjects.deleteAt(startline, endline - startline);
624 
625         // adjust offsets in layout for new height and offsets
626 
627         int ht = reflowed.getLineTop(n);
628         int toppad = 0, botpad = 0;
629 
630         if (mIncludePad && startline == 0) {
631             toppad = reflowed.getTopPadding();
632             mTopPadding = toppad;
633             ht -= toppad;
634         }
635         if (mIncludePad && islast) {
636             botpad = reflowed.getBottomPadding();
637             mBottomPadding = botpad;
638             ht += botpad;
639         }
640 
641         mInts.adjustValuesBelow(startline, START, after - before);
642         mInts.adjustValuesBelow(startline, TOP, startv - endv + ht);
643 
644         // insert new layout
645 
646         int[] ints;
647 
648         if (mEllipsize) {
649             ints = new int[COLUMNS_ELLIPSIZE];
650             ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
651         } else {
652             ints = new int[COLUMNS_NORMAL];
653         }
654 
655         Directions[] objects = new Directions[1];
656 
657         for (int i = 0; i < n; i++) {
658             final int start = reflowed.getLineStart(i);
659             ints[START] = start;
660             ints[DIR] |= reflowed.getParagraphDirection(i) << DIR_SHIFT;
661             ints[TAB] |= reflowed.getLineContainsTab(i) ? TAB_MASK : 0;
662 
663             int top = reflowed.getLineTop(i) + startv;
664             if (i > 0)
665                 top -= toppad;
666             ints[TOP] = top;
667 
668             int desc = reflowed.getLineDescent(i);
669             if (i == n - 1)
670                 desc += botpad;
671 
672             ints[DESCENT] = desc;
673             ints[EXTRA] = reflowed.getLineExtra(i);
674             objects[0] = reflowed.getLineDirections(i);
675 
676             final int end = (i == n - 1) ? where + after : reflowed.getLineStart(i + 1);
677             ints[HYPHEN] = StaticLayout.packHyphenEdit(
678                     reflowed.getStartHyphenEdit(i), reflowed.getEndHyphenEdit(i));
679             ints[MAY_PROTRUDE_FROM_TOP_OR_BOTTOM] |=
680                     contentMayProtrudeFromLineTopOrBottom(text, start, end) ?
681                             MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK : 0;
682 
683             if (mEllipsize) {
684                 ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
685                 ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i);
686             }
687 
688             mInts.insertAt(startline + i, ints);
689             mObjects.insertAt(startline + i, objects);
690         }
691 
692         updateBlocks(startline, endline - 1, n);
693 
694         b.finish();
695         synchronized (sLock) {
696             sStaticLayout = reflowed;
697             sBuilder = b;
698         }
699     }
700 
contentMayProtrudeFromLineTopOrBottom(CharSequence text, int start, int end)701     private boolean contentMayProtrudeFromLineTopOrBottom(CharSequence text, int start, int end) {
702         if (text instanceof Spanned) {
703             final Spanned spanned = (Spanned) text;
704             if (spanned.getSpans(start, end, ReplacementSpan.class).length > 0) {
705                 return true;
706             }
707         }
708         // Spans other than ReplacementSpan can be ignored because line top and bottom are
709         // disjunction of all tops and bottoms, although it's not optimal.
710         final Paint paint = getPaint();
711         if (text instanceof PrecomputedText) {
712             PrecomputedText precomputed = (PrecomputedText) text;
713             precomputed.getBounds(start, end, mTempRect);
714         } else {
715             paint.getTextBounds(text, start, end, mTempRect);
716         }
717         final Paint.FontMetricsInt fm = paint.getFontMetricsInt();
718         return mTempRect.top < fm.top || mTempRect.bottom > fm.bottom;
719     }
720 
721     /**
722      * Create the initial block structure, cutting the text into blocks of at least
723      * BLOCK_MINIMUM_CHARACTER_SIZE characters, aligned on the ends of paragraphs.
724      */
createBlocks()725     private void createBlocks() {
726         int offset = BLOCK_MINIMUM_CHARACTER_LENGTH;
727         mNumberOfBlocks = 0;
728         final CharSequence text = mDisplay;
729 
730         while (true) {
731             offset = TextUtils.indexOf(text, '\n', offset);
732             if (offset < 0) {
733                 addBlockAtOffset(text.length());
734                 break;
735             } else {
736                 addBlockAtOffset(offset);
737                 offset += BLOCK_MINIMUM_CHARACTER_LENGTH;
738             }
739         }
740 
741         // mBlockIndices and mBlockEndLines should have the same length
742         mBlockIndices = new int[mBlockEndLines.length];
743         for (int i = 0; i < mBlockEndLines.length; i++) {
744             mBlockIndices[i] = INVALID_BLOCK_INDEX;
745         }
746     }
747 
748     /**
749      * @hide
750      */
getBlocksAlwaysNeedToBeRedrawn()751     public ArraySet<Integer> getBlocksAlwaysNeedToBeRedrawn() {
752         return mBlocksAlwaysNeedToBeRedrawn;
753     }
754 
updateAlwaysNeedsToBeRedrawn(int blockIndex)755     private void updateAlwaysNeedsToBeRedrawn(int blockIndex) {
756         int startLine = blockIndex == 0 ? 0 : (mBlockEndLines[blockIndex - 1] + 1);
757         int endLine = mBlockEndLines[blockIndex];
758         for (int i = startLine; i <= endLine; i++) {
759             if (getContentMayProtrudeFromTopOrBottom(i)) {
760                 if (mBlocksAlwaysNeedToBeRedrawn == null) {
761                     mBlocksAlwaysNeedToBeRedrawn = new ArraySet<>();
762                 }
763                 mBlocksAlwaysNeedToBeRedrawn.add(blockIndex);
764                 return;
765             }
766         }
767         if (mBlocksAlwaysNeedToBeRedrawn != null) {
768             mBlocksAlwaysNeedToBeRedrawn.remove(blockIndex);
769         }
770     }
771 
772     /**
773      * Create a new block, ending at the specified character offset.
774      * A block will actually be created only if has at least one line, i.e. this offset is
775      * not on the end line of the previous block.
776      */
addBlockAtOffset(int offset)777     private void addBlockAtOffset(int offset) {
778         final int line = getLineForOffset(offset);
779         if (mBlockEndLines == null) {
780             // Initial creation of the array, no test on previous block ending line
781             mBlockEndLines = ArrayUtils.newUnpaddedIntArray(1);
782             mBlockEndLines[mNumberOfBlocks] = line;
783             updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
784             mNumberOfBlocks++;
785             return;
786         }
787 
788         final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1];
789         if (line > previousBlockEndLine) {
790             mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line);
791             updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
792             mNumberOfBlocks++;
793         }
794     }
795 
796     /**
797      * This method is called every time the layout is reflowed after an edition.
798      * It updates the internal block data structure. The text is split in blocks
799      * of contiguous lines, with at least one block for the entire text.
800      * When a range of lines is edited, new blocks (from 0 to 3 depending on the
801      * overlap structure) will replace the set of overlapping blocks.
802      * Blocks are listed in order and are represented by their ending line number.
803      * An index is associated to each block (which will be used by display lists),
804      * this class simply invalidates the index of blocks overlapping a modification.
805      *
806      * @param startLine the first line of the range of modified lines
807      * @param endLine the last line of the range, possibly equal to startLine, lower
808      * than getLineCount()
809      * @param newLineCount the number of lines that will replace the range, possibly 0
810      *
811      * @hide
812      */
813     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
updateBlocks(int startLine, int endLine, int newLineCount)814     public void updateBlocks(int startLine, int endLine, int newLineCount) {
815         if (mBlockEndLines == null) {
816             createBlocks();
817             return;
818         }
819 
820         /*final*/ int firstBlock = -1;
821         /*final*/ int lastBlock = -1;
822         for (int i = 0; i < mNumberOfBlocks; i++) {
823             if (mBlockEndLines[i] >= startLine) {
824                 firstBlock = i;
825                 break;
826             }
827         }
828         for (int i = firstBlock; i < mNumberOfBlocks; i++) {
829             if (mBlockEndLines[i] >= endLine) {
830                 lastBlock = i;
831                 break;
832             }
833         }
834         final int lastBlockEndLine = mBlockEndLines[lastBlock];
835 
836         final boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
837                 mBlockEndLines[firstBlock - 1] + 1);
838         final boolean createBlock = newLineCount > 0;
839         final boolean createBlockAfter = endLine < mBlockEndLines[lastBlock];
840 
841         int numAddedBlocks = 0;
842         if (createBlockBefore) numAddedBlocks++;
843         if (createBlock) numAddedBlocks++;
844         if (createBlockAfter) numAddedBlocks++;
845 
846         final int numRemovedBlocks = lastBlock - firstBlock + 1;
847         final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks;
848 
849         if (newNumberOfBlocks == 0) {
850             // Even when text is empty, there is actually one line and hence one block
851             mBlockEndLines[0] = 0;
852             mBlockIndices[0] = INVALID_BLOCK_INDEX;
853             mNumberOfBlocks = 1;
854             return;
855         }
856 
857         if (newNumberOfBlocks > mBlockEndLines.length) {
858             int[] blockEndLines = ArrayUtils.newUnpaddedIntArray(
859                     Math.max(mBlockEndLines.length * 2, newNumberOfBlocks));
860             int[] blockIndices = new int[blockEndLines.length];
861             System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock);
862             System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock);
863             System.arraycopy(mBlockEndLines, lastBlock + 1,
864                     blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
865             System.arraycopy(mBlockIndices, lastBlock + 1,
866                     blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
867             mBlockEndLines = blockEndLines;
868             mBlockIndices = blockIndices;
869         } else if (numAddedBlocks + numRemovedBlocks != 0) {
870             System.arraycopy(mBlockEndLines, lastBlock + 1,
871                     mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
872             System.arraycopy(mBlockIndices, lastBlock + 1,
873                     mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
874         }
875 
876         if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) {
877             final ArraySet<Integer> set = new ArraySet<>();
878             final int changedBlockCount = numAddedBlocks - numRemovedBlocks;
879             for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) {
880                 Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i);
881                 if (block < firstBlock) {
882                     // block index is before firstBlock add it since it did not change
883                     set.add(block);
884                 }
885                 if (block > lastBlock) {
886                     // block index is after lastBlock, the index reduced to += changedBlockCount
887                     block += changedBlockCount;
888                     set.add(block);
889                 }
890             }
891             mBlocksAlwaysNeedToBeRedrawn = set;
892         }
893 
894         mNumberOfBlocks = newNumberOfBlocks;
895         int newFirstChangedBlock;
896         final int deltaLines = newLineCount - (endLine - startLine + 1);
897         if (deltaLines != 0) {
898             // Display list whose index is >= mIndexFirstChangedBlock is valid
899             // but it needs to update its drawing location.
900             newFirstChangedBlock = firstBlock + numAddedBlocks;
901             for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) {
902                 mBlockEndLines[i] += deltaLines;
903             }
904         } else {
905             newFirstChangedBlock = mNumberOfBlocks;
906         }
907         mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock);
908 
909         int blockIndex = firstBlock;
910         if (createBlockBefore) {
911             mBlockEndLines[blockIndex] = startLine - 1;
912             updateAlwaysNeedsToBeRedrawn(blockIndex);
913             mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
914             blockIndex++;
915         }
916 
917         if (createBlock) {
918             mBlockEndLines[blockIndex] = startLine + newLineCount - 1;
919             updateAlwaysNeedsToBeRedrawn(blockIndex);
920             mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
921             blockIndex++;
922         }
923 
924         if (createBlockAfter) {
925             mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines;
926             updateAlwaysNeedsToBeRedrawn(blockIndex);
927             mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
928         }
929     }
930 
931     /**
932      * This method is used for test purposes only.
933      * @hide
934      */
935     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks, int totalLines)936     public void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks,
937             int totalLines) {
938         mBlockEndLines = new int[blockEndLines.length];
939         mBlockIndices = new int[blockIndices.length];
940         System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length);
941         System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length);
942         mNumberOfBlocks = numberOfBlocks;
943         while (mInts.size() < totalLines) {
944             mInts.insertAt(mInts.size(), new int[COLUMNS_NORMAL]);
945         }
946     }
947 
948     /**
949      * @hide
950      */
951     @UnsupportedAppUsage
getBlockEndLines()952     public int[] getBlockEndLines() {
953         return mBlockEndLines;
954     }
955 
956     /**
957      * @hide
958      */
959     @UnsupportedAppUsage
getBlockIndices()960     public int[] getBlockIndices() {
961         return mBlockIndices;
962     }
963 
964     /**
965      * @hide
966      */
getBlockIndex(int index)967     public int getBlockIndex(int index) {
968         return mBlockIndices[index];
969     }
970 
971     /**
972      * @hide
973      * @param index
974      */
setBlockIndex(int index, int blockIndex)975     public void setBlockIndex(int index, int blockIndex) {
976         mBlockIndices[index] = blockIndex;
977     }
978 
979     /**
980      * @hide
981      */
982     @UnsupportedAppUsage
getNumberOfBlocks()983     public int getNumberOfBlocks() {
984         return mNumberOfBlocks;
985     }
986 
987     /**
988      * @hide
989      */
990     @UnsupportedAppUsage
getIndexFirstChangedBlock()991     public int getIndexFirstChangedBlock() {
992         return mIndexFirstChangedBlock;
993     }
994 
995     /**
996      * @hide
997      */
998     @UnsupportedAppUsage
setIndexFirstChangedBlock(int i)999     public void setIndexFirstChangedBlock(int i) {
1000         mIndexFirstChangedBlock = i;
1001     }
1002 
1003     @Override
getLineCount()1004     public int getLineCount() {
1005         return mInts.size() - 1;
1006     }
1007 
1008     @Override
getLineTop(int line)1009     public int getLineTop(int line) {
1010         return mInts.getValue(line, TOP);
1011     }
1012 
1013     @Override
getLineDescent(int line)1014     public int getLineDescent(int line) {
1015         return mInts.getValue(line, DESCENT);
1016     }
1017 
1018     /**
1019      * @hide
1020      */
1021     @Override
getLineExtra(int line)1022     public int getLineExtra(int line) {
1023         return mInts.getValue(line, EXTRA);
1024     }
1025 
1026     @Override
getLineStart(int line)1027     public int getLineStart(int line) {
1028         return mInts.getValue(line, START) & START_MASK;
1029     }
1030 
1031     @Override
getLineContainsTab(int line)1032     public boolean getLineContainsTab(int line) {
1033         return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
1034     }
1035 
1036     @Override
getParagraphDirection(int line)1037     public int getParagraphDirection(int line) {
1038         return mInts.getValue(line, DIR) >> DIR_SHIFT;
1039     }
1040 
1041     @Override
getLineDirections(int line)1042     public final Directions getLineDirections(int line) {
1043         return mObjects.getValue(line, 0);
1044     }
1045 
1046     @Override
getTopPadding()1047     public int getTopPadding() {
1048         return mTopPadding;
1049     }
1050 
1051     @Override
getBottomPadding()1052     public int getBottomPadding() {
1053         return mBottomPadding;
1054     }
1055 
1056     /**
1057      * @hide
1058      */
1059     @Override
getStartHyphenEdit(int line)1060     public @Paint.StartHyphenEdit int getStartHyphenEdit(int line) {
1061         return StaticLayout.unpackStartHyphenEdit(mInts.getValue(line, HYPHEN) & HYPHEN_MASK);
1062     }
1063 
1064     /**
1065      * @hide
1066      */
1067     @Override
getEndHyphenEdit(int line)1068     public @Paint.EndHyphenEdit int getEndHyphenEdit(int line) {
1069         return StaticLayout.unpackEndHyphenEdit(mInts.getValue(line, HYPHEN) & HYPHEN_MASK);
1070     }
1071 
getContentMayProtrudeFromTopOrBottom(int line)1072     private boolean getContentMayProtrudeFromTopOrBottom(int line) {
1073         return (mInts.getValue(line, MAY_PROTRUDE_FROM_TOP_OR_BOTTOM)
1074                 & MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK) != 0;
1075     }
1076 
1077     @Override
getEllipsizedWidth()1078     public int getEllipsizedWidth() {
1079         return mEllipsizedWidth;
1080     }
1081 
1082     private static class ChangeWatcher implements TextWatcher, SpanWatcher {
ChangeWatcher(DynamicLayout layout)1083         public ChangeWatcher(DynamicLayout layout) {
1084             mLayout = new WeakReference<>(layout);
1085         }
1086 
reflow(CharSequence s, int where, int before, int after)1087         private void reflow(CharSequence s, int where, int before, int after) {
1088             DynamicLayout ml = mLayout.get();
1089 
1090             if (ml != null) {
1091                 ml.reflow(s, where, before, after);
1092             } else if (s instanceof Spannable) {
1093                 ((Spannable) s).removeSpan(this);
1094             }
1095         }
1096 
beforeTextChanged(CharSequence s, int where, int before, int after)1097         public void beforeTextChanged(CharSequence s, int where, int before, int after) {
1098             // Intentionally empty
1099         }
1100 
onTextChanged(CharSequence s, int where, int before, int after)1101         public void onTextChanged(CharSequence s, int where, int before, int after) {
1102             reflow(s, where, before, after);
1103         }
1104 
afterTextChanged(Editable s)1105         public void afterTextChanged(Editable s) {
1106             // Intentionally empty
1107         }
1108 
onSpanAdded(Spannable s, Object o, int start, int end)1109         public void onSpanAdded(Spannable s, Object o, int start, int end) {
1110             if (o instanceof UpdateLayout)
1111                 reflow(s, start, end - start, end - start);
1112         }
1113 
onSpanRemoved(Spannable s, Object o, int start, int end)1114         public void onSpanRemoved(Spannable s, Object o, int start, int end) {
1115             if (o instanceof UpdateLayout)
1116                 reflow(s, start, end - start, end - start);
1117         }
1118 
onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend)1119         public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) {
1120             if (o instanceof UpdateLayout) {
1121                 if (start > end) {
1122                     // Bug: 67926915 start cannot be determined, fallback to reflow from start
1123                     // instead of causing an exception
1124                     start = 0;
1125                 }
1126                 reflow(s, start, end - start, end - start);
1127                 reflow(s, nstart, nend - nstart, nend - nstart);
1128             }
1129         }
1130 
1131         private WeakReference<DynamicLayout> mLayout;
1132     }
1133 
1134     @Override
getEllipsisStart(int line)1135     public int getEllipsisStart(int line) {
1136         if (mEllipsizeAt == null) {
1137             return 0;
1138         }
1139 
1140         return mInts.getValue(line, ELLIPSIS_START);
1141     }
1142 
1143     @Override
getEllipsisCount(int line)1144     public int getEllipsisCount(int line) {
1145         if (mEllipsizeAt == null) {
1146             return 0;
1147         }
1148 
1149         return mInts.getValue(line, ELLIPSIS_COUNT);
1150     }
1151 
1152     private CharSequence mBase;
1153     private CharSequence mDisplay;
1154     private ChangeWatcher mWatcher;
1155     private boolean mIncludePad;
1156     private boolean mFallbackLineSpacing;
1157     private boolean mEllipsize;
1158     private int mEllipsizedWidth;
1159     private TextUtils.TruncateAt mEllipsizeAt;
1160     private int mBreakStrategy;
1161     private int mHyphenationFrequency;
1162     private int mJustificationMode;
1163 
1164     private PackedIntVector mInts;
1165     private PackedObjectVector<Directions> mObjects;
1166 
1167     /**
1168      * Value used in mBlockIndices when a block has been created or recycled and indicating that its
1169      * display list needs to be re-created.
1170      * @hide
1171      */
1172     public static final int INVALID_BLOCK_INDEX = -1;
1173     // Stores the line numbers of the last line of each block (inclusive)
1174     private int[] mBlockEndLines;
1175     // The indices of this block's display list in TextView's internal display list array or
1176     // INVALID_BLOCK_INDEX if this block has been invalidated during an edition
1177     private int[] mBlockIndices;
1178     // Set of blocks that always need to be redrawn.
1179     private ArraySet<Integer> mBlocksAlwaysNeedToBeRedrawn;
1180     // Number of items actually currently being used in the above 2 arrays
1181     private int mNumberOfBlocks;
1182     // The first index of the blocks whose locations are changed
1183     private int mIndexFirstChangedBlock;
1184 
1185     private int mTopPadding, mBottomPadding;
1186 
1187     private Rect mTempRect = new Rect();
1188 
1189     @UnsupportedAppUsage
1190     private static StaticLayout sStaticLayout = null;
1191     private static StaticLayout.Builder sBuilder = null;
1192 
1193     private static final Object[] sLock = new Object[0];
1194 
1195     // START, DIR, and TAB share the same entry.
1196     private static final int START = 0;
1197     private static final int DIR = START;
1198     private static final int TAB = START;
1199     private static final int TOP = 1;
1200     private static final int DESCENT = 2;
1201     private static final int EXTRA = 3;
1202     // HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry.
1203     private static final int HYPHEN = 4;
1204     private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN;
1205     private static final int COLUMNS_NORMAL = 5;
1206 
1207     private static final int ELLIPSIS_START = 5;
1208     private static final int ELLIPSIS_COUNT = 6;
1209     private static final int COLUMNS_ELLIPSIZE = 7;
1210 
1211     private static final int START_MASK = 0x1FFFFFFF;
1212     private static final int DIR_SHIFT  = 30;
1213     private static final int TAB_MASK   = 0x20000000;
1214     private static final int HYPHEN_MASK = 0xFF;
1215     private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK = 0x100;
1216 
1217     private static final int ELLIPSIS_UNDEFINED = 0x80000000;
1218 }
1219