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