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