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