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