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_USE_BOUNDS_FOR_WIDTH;
21 
22 import android.annotation.FlaggedApi;
23 import android.annotation.FloatRange;
24 import android.annotation.IntRange;
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.annotation.SuppressLint;
28 import android.compat.annotation.UnsupportedAppUsage;
29 import android.graphics.Paint;
30 import android.graphics.RectF;
31 import android.graphics.text.LineBreakConfig;
32 import android.graphics.text.LineBreaker;
33 import android.os.Build;
34 import android.os.Trace;
35 import android.text.style.LeadingMarginSpan;
36 import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
37 import android.text.style.LineHeightSpan;
38 import android.text.style.TabStopSpan;
39 import android.util.Log;
40 import android.util.Pools.SynchronizedPool;
41 
42 import com.android.internal.util.ArrayUtils;
43 import com.android.internal.util.GrowingArrayUtils;
44 
45 import java.util.Arrays;
46 
47 /**
48  * StaticLayout is a Layout for text that will not be edited after it
49  * is laid out.  Use {@link DynamicLayout} for text that may change.
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 would be tempted to call
53  * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int,
54  * float, float, android.graphics.Paint)
55  * Canvas.drawText()} directly.</p>
56  */
57 public class StaticLayout extends Layout {
58     /*
59      * The break iteration is done in native code. The protocol for using the native code is as
60      * follows.
61      *
62      * First, call nInit to setup native line breaker object. Then, for each paragraph, do the
63      * following:
64      *
65      *   - Create MeasuredParagraph by MeasuredParagraph.buildForStaticLayout which measures in
66      *     native.
67      *   - Run LineBreaker.computeLineBreaks() to obtain line breaks for the paragraph.
68      *
69      * After all paragraphs, call finish() to release expensive buffers.
70      */
71 
72     static final String TAG = "StaticLayout";
73 
74     /**
75      * Builder for static layouts. The builder is the preferred pattern for constructing
76      * StaticLayout objects and should be preferred over the constructors, particularly to access
77      * newer features. To build a static layout, first call {@link #obtain} with the required
78      * arguments (text, paint, and width), then call setters for optional parameters, and finally
79      * {@link #build} to build the StaticLayout object. Parameters not explicitly set will get
80      * default values.
81      */
82     public final static class Builder {
Builder()83         private Builder() {}
84 
85         /**
86          * Obtain a builder for constructing StaticLayout objects.
87          *
88          * @param source The text to be laid out, optionally with spans
89          * @param start The index of the start of the text
90          * @param end The index + 1 of the end of the text
91          * @param paint The base paint used for layout
92          * @param width The width in pixels
93          * @return a builder object used for constructing the StaticLayout
94          */
95         @NonNull
obtain(@onNull CharSequence source, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextPaint paint, @IntRange(from = 0) int width)96         public static Builder obtain(@NonNull CharSequence source, @IntRange(from = 0) int start,
97                 @IntRange(from = 0) int end, @NonNull TextPaint paint,
98                 @IntRange(from = 0) int width) {
99             Builder b = sPool.acquire();
100             if (b == null) {
101                 b = new Builder();
102             }
103 
104             // set default initial values
105             b.mText = source;
106             b.mStart = start;
107             b.mEnd = end;
108             b.mPaint = paint;
109             b.mWidth = width;
110             b.mAlignment = Alignment.ALIGN_NORMAL;
111             b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
112             b.mSpacingMult = DEFAULT_LINESPACING_MULTIPLIER;
113             b.mSpacingAdd = DEFAULT_LINESPACING_ADDITION;
114             b.mIncludePad = true;
115             b.mFallbackLineSpacing = false;
116             b.mEllipsizedWidth = width;
117             b.mEllipsize = null;
118             b.mMaxLines = Integer.MAX_VALUE;
119             b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
120             b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
121             b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
122             b.mLineBreakConfig = LineBreakConfig.NONE;
123             b.mMinimumFontMetrics = null;
124             return b;
125         }
126 
127         /**
128          * This method should be called after the layout is finished getting constructed and the
129          * builder needs to be cleaned up and returned to the pool.
130          */
recycle(@onNull Builder b)131         private static void recycle(@NonNull Builder b) {
132             b.mPaint = null;
133             b.mText = null;
134             b.mLeftIndents = null;
135             b.mRightIndents = null;
136             b.mMinimumFontMetrics = null;
137             sPool.release(b);
138         }
139 
140         // release any expensive state
finish()141         /* package */ void finish() {
142             mText = null;
143             mPaint = null;
144             mLeftIndents = null;
145             mRightIndents = null;
146             mMinimumFontMetrics = null;
147         }
148 
setText(CharSequence source)149         public Builder setText(CharSequence source) {
150             return setText(source, 0, source.length());
151         }
152 
153         /**
154          * Set the text. Only useful when re-using the builder, which is done for
155          * the internal implementation of {@link DynamicLayout} but not as part
156          * of normal {@link StaticLayout} usage.
157          *
158          * @param source The text to be laid out, optionally with spans
159          * @param start The index of the start of the text
160          * @param end The index + 1 of the end of the text
161          * @return this builder, useful for chaining
162          *
163          * @hide
164          */
165         @NonNull
setText(@onNull CharSequence source, int start, int end)166         public Builder setText(@NonNull CharSequence source, int start, int end) {
167             mText = source;
168             mStart = start;
169             mEnd = end;
170             return this;
171         }
172 
173         /**
174          * Set the paint. Internal for reuse cases only.
175          *
176          * @param paint The base paint used for layout
177          * @return this builder, useful for chaining
178          *
179          * @hide
180          */
181         @NonNull
setPaint(@onNull TextPaint paint)182         public Builder setPaint(@NonNull TextPaint paint) {
183             mPaint = paint;
184             return this;
185         }
186 
187         /**
188          * Set the width. Internal for reuse cases only.
189          *
190          * @param width The width in pixels
191          * @return this builder, useful for chaining
192          *
193          * @hide
194          */
195         @NonNull
setWidth(@ntRangefrom = 0) int width)196         public Builder setWidth(@IntRange(from = 0) int width) {
197             mWidth = width;
198             if (mEllipsize == null) {
199                 mEllipsizedWidth = width;
200             }
201             return this;
202         }
203 
204         /**
205          * Set the alignment. The default is {@link Layout.Alignment#ALIGN_NORMAL}.
206          *
207          * @param alignment Alignment for the resulting {@link StaticLayout}
208          * @return this builder, useful for chaining
209          */
210         @NonNull
setAlignment(@onNull Alignment alignment)211         public Builder setAlignment(@NonNull Alignment alignment) {
212             mAlignment = alignment;
213             return this;
214         }
215 
216         /**
217          * Set the text direction heuristic. The text direction heuristic is used to
218          * resolve text direction per-paragraph based on the input text. The default is
219          * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}.
220          *
221          * @param textDir text direction heuristic for resolving bidi behavior.
222          * @return this builder, useful for chaining
223          */
224         @NonNull
setTextDirection(@onNull TextDirectionHeuristic textDir)225         public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) {
226             mTextDir = textDir;
227             return this;
228         }
229 
230         /**
231          * Set line spacing parameters. Each line will have its line spacing multiplied by
232          * {@code spacingMult} and then increased by {@code spacingAdd}. The default is 0.0 for
233          * {@code spacingAdd} and 1.0 for {@code spacingMult}.
234          *
235          * @param spacingAdd the amount of line spacing addition
236          * @param spacingMult the line spacing multiplier
237          * @return this builder, useful for chaining
238          * @see android.widget.TextView#setLineSpacing
239          */
240         @NonNull
setLineSpacing(float spacingAdd, @FloatRange(from = 0.0) float spacingMult)241         public Builder setLineSpacing(float spacingAdd, @FloatRange(from = 0.0) float spacingMult) {
242             mSpacingAdd = spacingAdd;
243             mSpacingMult = spacingMult;
244             return this;
245         }
246 
247         /**
248          * Set whether to include extra space beyond font ascent and descent (which is
249          * needed to avoid clipping in some languages, such as Arabic and Kannada). The
250          * default is {@code true}.
251          *
252          * @param includePad whether to include padding
253          * @return this builder, useful for chaining
254          * @see android.widget.TextView#setIncludeFontPadding
255          */
256         @NonNull
setIncludePad(boolean includePad)257         public Builder setIncludePad(boolean includePad) {
258             mIncludePad = includePad;
259             return this;
260         }
261 
262         /**
263          * Set whether to respect the ascent and descent of the fallback fonts that are used in
264          * displaying the text (which is needed to avoid text from consecutive lines running into
265          * each other). If set, fallback fonts that end up getting used can increase the ascent
266          * and descent of the lines that they are used on.
267          *
268          * <p>For backward compatibility reasons, the default is {@code false}, but setting this to
269          * true is strongly recommended. It is required to be true if text could be in languages
270          * like Burmese or Tibetan where text is typically much taller or deeper than Latin text.
271          *
272          * @param useLineSpacingFromFallbacks whether to expand linespacing based on fallback fonts
273          * @return this builder, useful for chaining
274          */
275         @NonNull
setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks)276         public Builder setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks) {
277             mFallbackLineSpacing = useLineSpacingFromFallbacks;
278             return this;
279         }
280 
281         /**
282          * Set the width as used for ellipsizing purposes, if it differs from the
283          * normal layout width. The default is the {@code width}
284          * passed to {@link #obtain}.
285          *
286          * @param ellipsizedWidth width used for ellipsizing, in pixels
287          * @return this builder, useful for chaining
288          * @see android.widget.TextView#setEllipsize
289          */
290         @NonNull
setEllipsizedWidth(@ntRangefrom = 0) int ellipsizedWidth)291         public Builder setEllipsizedWidth(@IntRange(from = 0) int ellipsizedWidth) {
292             mEllipsizedWidth = ellipsizedWidth;
293             return this;
294         }
295 
296         /**
297          * Set ellipsizing on the layout. Causes words that are longer than the view
298          * is wide, or exceeding the number of lines (see #setMaxLines) in the case
299          * of {@link android.text.TextUtils.TruncateAt#END} or
300          * {@link android.text.TextUtils.TruncateAt#MARQUEE}, to be ellipsized instead
301          * of broken. The default is {@code null}, indicating no ellipsis is to be applied.
302          *
303          * @param ellipsize type of ellipsis behavior
304          * @return this builder, useful for chaining
305          * @see android.widget.TextView#setEllipsize
306          */
307         @NonNull
setEllipsize(@ullable TextUtils.TruncateAt ellipsize)308         public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) {
309             mEllipsize = ellipsize;
310             return this;
311         }
312 
313         /**
314          * Set maximum number of lines. This is particularly useful in the case of
315          * ellipsizing, where it changes the layout of the last line. The default is
316          * unlimited.
317          *
318          * @param maxLines maximum number of lines in the layout
319          * @return this builder, useful for chaining
320          * @see android.widget.TextView#setMaxLines
321          */
322         @NonNull
setMaxLines(@ntRangefrom = 0) int maxLines)323         public Builder setMaxLines(@IntRange(from = 0) int maxLines) {
324             mMaxLines = maxLines;
325             return this;
326         }
327 
328         /**
329          * Set break strategy, useful for selecting high quality or balanced paragraph
330          * layout options. The default is {@link Layout#BREAK_STRATEGY_SIMPLE}.
331          * <p/>
332          * Enabling hyphenation with either using {@link Layout#HYPHENATION_FREQUENCY_NORMAL} or
333          * {@link Layout#HYPHENATION_FREQUENCY_FULL} while line breaking is set to one of
334          * {@link Layout#BREAK_STRATEGY_BALANCED}, {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}
335          * improves the structure of text layout however has performance impact and requires more
336          * time to do the text layout.
337          *
338          * @param breakStrategy break strategy for paragraph layout
339          * @return this builder, useful for chaining
340          * @see android.widget.TextView#setBreakStrategy
341          * @see #setHyphenationFrequency(int)
342          */
343         @NonNull
setBreakStrategy(@reakStrategy int breakStrategy)344         public Builder setBreakStrategy(@BreakStrategy int breakStrategy) {
345             mBreakStrategy = breakStrategy;
346             return this;
347         }
348 
349         /**
350          * Set hyphenation frequency, to control the amount of automatic hyphenation used. The
351          * possible values are defined in {@link Layout}, by constants named with the pattern
352          * {@code HYPHENATION_FREQUENCY_*}. The default is
353          * {@link Layout#HYPHENATION_FREQUENCY_NONE}.
354          * <p/>
355          * Enabling hyphenation with either using {@link Layout#HYPHENATION_FREQUENCY_NORMAL} or
356          * {@link Layout#HYPHENATION_FREQUENCY_FULL} while line breaking is set to one of
357          * {@link Layout#BREAK_STRATEGY_BALANCED}, {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}
358          * improves the structure of text layout however has performance impact and requires more
359          * time to do the text layout.
360          *
361          * @param hyphenationFrequency hyphenation frequency for the paragraph
362          * @return this builder, useful for chaining
363          * @see android.widget.TextView#setHyphenationFrequency
364          * @see #setBreakStrategy(int)
365          */
366         @NonNull
setHyphenationFrequency(@yphenationFrequency int hyphenationFrequency)367         public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) {
368             mHyphenationFrequency = hyphenationFrequency;
369             return this;
370         }
371 
372         /**
373          * Set indents. Arguments are arrays holding an indent amount, one per line, measured in
374          * pixels. For lines past the last element in the array, the last element repeats.
375          *
376          * @param leftIndents array of indent values for left margin, in pixels
377          * @param rightIndents array of indent values for right margin, in pixels
378          * @return this builder, useful for chaining
379          */
380         @NonNull
setIndents(@ullable int[] leftIndents, @Nullable int[] rightIndents)381         public Builder setIndents(@Nullable int[] leftIndents, @Nullable int[] rightIndents) {
382             mLeftIndents = leftIndents;
383             mRightIndents = rightIndents;
384             return this;
385         }
386 
387         /**
388          * Set paragraph justification mode. The default value is
389          * {@link Layout#JUSTIFICATION_MODE_NONE}. If the last line is too short for justification,
390          * the last line will be displayed with the alignment set by {@link #setAlignment}.
391          * When Justification mode is JUSTIFICATION_MODE_INTER_WORD, wordSpacing on the given
392          * {@link Paint} will be ignored. This behavior also affects Spans which change the
393          * wordSpacing.
394          *
395          * @param justificationMode justification mode for the paragraph.
396          * @return this builder, useful for chaining.
397          * @see Paint#setWordSpacing(float)
398          */
399         @NonNull
setJustificationMode(@ustificationMode int justificationMode)400         public Builder setJustificationMode(@JustificationMode int justificationMode) {
401             mJustificationMode = justificationMode;
402             return this;
403         }
404 
405         /**
406          * Sets whether the line spacing should be applied for the last line. Default value is
407          * {@code false}.
408          *
409          * @hide
410          */
411         @NonNull
setAddLastLineLineSpacing(boolean value)412         /* package */ Builder setAddLastLineLineSpacing(boolean value) {
413             mAddLastLineLineSpacing = value;
414             return this;
415         }
416 
417         /**
418          * Set the line break configuration. The line break will be passed to native used for
419          * calculating the text wrapping. The default value of the line break style is
420          * {@link LineBreakConfig#LINE_BREAK_STYLE_NONE}
421          *
422          * @param lineBreakConfig the line break configuration for text wrapping.
423          * @return this builder, useful for chaining.
424          * @see android.widget.TextView#setLineBreakStyle
425          * @see android.widget.TextView#setLineBreakWordStyle
426          */
427         @NonNull
setLineBreakConfig(@onNull LineBreakConfig lineBreakConfig)428         public Builder setLineBreakConfig(@NonNull LineBreakConfig lineBreakConfig) {
429             mLineBreakConfig = lineBreakConfig;
430             return this;
431         }
432 
433         /**
434          * Set true for using width of bounding box as a source of automatic line breaking and
435          * drawing.
436          *
437          * If this value is false, the Layout determines the drawing offset and automatic line
438          * breaking based on total advances. By setting true, use all joined glyph's bounding boxes
439          * as a source of text width.
440          *
441          * If the font has glyphs that have negative bearing X or its xMax is greater than advance,
442          * the glyph clipping can happen because the drawing area may be bigger. By setting this to
443          * true, the Layout will reserve more spaces for drawing.
444          *
445          * @param useBoundsForWidth True for using bounding box, false for advances.
446          * @return this builder instance
447          * @see Layout#getUseBoundsForWidth()
448          * @see Layout.Builder#setUseBoundsForWidth(boolean)
449          */
450         @SuppressLint("MissingGetterMatchingBuilder")  // The base class `Layout` has a getter.
451         @NonNull
452         @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
setUseBoundsForWidth(boolean useBoundsForWidth)453         public Builder setUseBoundsForWidth(boolean useBoundsForWidth) {
454             mUseBoundsForWidth = useBoundsForWidth;
455             return this;
456         }
457 
458         /**
459          * Set true for shifting the drawing x offset for showing overhang at the start position.
460          *
461          * This flag is ignored if the {@link #getUseBoundsForWidth()} is false.
462          *
463          * If this value is false, the Layout draws text from the zero even if there is a glyph
464          * stroke in a region where the x coordinate is negative.
465          *
466          * If this value is true, the Layout draws text with shifting the x coordinate of the
467          * drawing bounding box.
468          *
469          * This value is false by default.
470          *
471          * @param shiftDrawingOffsetForStartOverhang true for shifting the drawing offset for
472          *                                          showing the stroke that is in the region where
473          *                                          the x coordinate is negative.
474          * @see #setUseBoundsForWidth(boolean)
475          * @see #getUseBoundsForWidth()
476          */
477         @NonNull
478         // The corresponding getter is getShiftDrawingOffsetForStartOverhang()
479         @SuppressLint("MissingGetterMatchingBuilder")
480         @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
setShiftDrawingOffsetForStartOverhang( boolean shiftDrawingOffsetForStartOverhang)481         public Builder setShiftDrawingOffsetForStartOverhang(
482                 boolean shiftDrawingOffsetForStartOverhang) {
483             mShiftDrawingOffsetForStartOverhang = shiftDrawingOffsetForStartOverhang;
484             return this;
485         }
486 
487         /**
488          * Internal API that tells underlying line breaker that calculating bounding boxes even if
489          * the line break is performed with advances. This is useful for DynamicLayout internal
490          * implementation because it uses bounding box as well as advances.
491          * @hide
492          */
setCalculateBounds(boolean value)493         public Builder setCalculateBounds(boolean value) {
494             mCalculateBounds = value;
495             return this;
496         }
497 
498         /**
499          * Set the minimum font metrics used for line spacing.
500          *
501          * <p>
502          * {@code null} is the default value. If {@code null} is set or left as default, the
503          * font metrics obtained by {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} is
504          * used.
505          *
506          * <p>
507          * The minimum meaning here is the minimum value of line spacing: maximum value of
508          * {@link Paint#ascent()}, minimum value of {@link Paint#descent()}.
509          *
510          * <p>
511          * By setting this value, each line will have minimum line spacing regardless of the text
512          * rendered. For example, usually Japanese script has larger vertical metrics than Latin
513          * script. By setting the metrics obtained by
514          * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} for Japanese or leave it
515          * {@code null} if the Paint's locale is Japanese, the line spacing for Japanese is reserved
516          * if the text is an English text. If the vertical metrics of the text is larger than
517          * Japanese, for example Burmese, the bigger font metrics is used.
518          *
519          * @param minimumFontMetrics A minimum font metrics. Passing {@code null} for using the
520          *                          value obtained by
521          *                          {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)}
522          * @see android.widget.TextView#setMinimumFontMetrics(Paint.FontMetrics)
523          * @see android.widget.TextView#getMinimumFontMetrics()
524          * @see Layout#getMinimumFontMetrics()
525          * @see Layout.Builder#setMinimumFontMetrics(Paint.FontMetrics)
526          * @see DynamicLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics)
527          */
528         @NonNull
529         @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE)
setMinimumFontMetrics(@ullable Paint.FontMetrics minimumFontMetrics)530         public Builder setMinimumFontMetrics(@Nullable Paint.FontMetrics minimumFontMetrics) {
531             mMinimumFontMetrics = minimumFontMetrics;
532             return this;
533         }
534 
535         /**
536          * Build the {@link StaticLayout} after options have been set.
537          *
538          * <p>Note: the builder object must not be reused in any way after calling this
539          * method. Setting parameters after calling this method, or calling it a second
540          * time on the same builder object, will likely lead to unexpected results.
541          *
542          * @return the newly constructed {@link StaticLayout} object
543          */
544         @NonNull
build()545         public StaticLayout build() {
546             StaticLayout result = new StaticLayout(this, mIncludePad, mEllipsize != null
547                     ? COLUMNS_ELLIPSIZE : COLUMNS_NORMAL);
548             Builder.recycle(this);
549             return result;
550         }
551 
552         /**
553          * DO NOT USE THIS METHOD OTHER THAN DynamicLayout.
554          *
555          * This class generates a very weird StaticLayout only for getting a result of line break.
556          * Since DynamicLayout keeps StaticLayout reference in the static context for object
557          * recycling but keeping text reference in static context will end up with leaking Context
558          * due to TextWatcher via TextView.
559          *
560          * So, this is a dirty work around that creating StaticLayout without passing text reference
561          * to the super constructor, but calculating the text layout by calling generate function
562          * directly.
563          */
buildPartialStaticLayoutForDynamicLayout( boolean trackpadding, StaticLayout recycle)564         /* package */ @NonNull StaticLayout buildPartialStaticLayoutForDynamicLayout(
565                 boolean trackpadding, StaticLayout recycle) {
566             if (recycle == null) {
567                 recycle = new StaticLayout();
568             }
569             Trace.beginSection("Generating StaticLayout For DynamicLayout");
570             try {
571                 recycle.generate(this, mIncludePad, trackpadding);
572             } finally {
573                 Trace.endSection();
574             }
575             return recycle;
576         }
577 
578         private CharSequence mText;
579         private int mStart;
580         private int mEnd;
581         private TextPaint mPaint;
582         private int mWidth;
583         private Alignment mAlignment;
584         private TextDirectionHeuristic mTextDir;
585         private float mSpacingMult;
586         private float mSpacingAdd;
587         private boolean mIncludePad;
588         private boolean mFallbackLineSpacing;
589         private int mEllipsizedWidth;
590         private TextUtils.TruncateAt mEllipsize;
591         private int mMaxLines;
592         private int mBreakStrategy;
593         private int mHyphenationFrequency;
594         @Nullable private int[] mLeftIndents;
595         @Nullable private int[] mRightIndents;
596         private int mJustificationMode;
597         private boolean mAddLastLineLineSpacing;
598         private LineBreakConfig mLineBreakConfig = LineBreakConfig.NONE;
599         private boolean mUseBoundsForWidth;
600         private boolean mShiftDrawingOffsetForStartOverhang;
601         private boolean mCalculateBounds;
602         @Nullable private Paint.FontMetrics mMinimumFontMetrics;
603 
604         private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
605 
606         private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<>(3);
607     }
608 
609     /**
610      * DO NOT USE THIS CONSTRUCTOR OTHER THAN FOR DYNAMIC LAYOUT.
611      * See Builder#buildPartialStaticLayoutForDynamicLayout for the reason of this constructor.
612      */
StaticLayout()613     private StaticLayout() {
614         super(
615                 null,  // text
616                 null,  // paint
617                 0,  // width
618                 null, // alignment
619                 null, // textDir
620                 1, // spacing multiplier
621                 0, // spacing amount
622                 false, // include font padding
623                 false, // fallback line spacing
624                 0,  // ellipsized width
625                 null, // ellipsize
626                 1,  // maxLines
627                 BREAK_STRATEGY_SIMPLE,
628                 HYPHENATION_FREQUENCY_NONE,
629                 null,  // leftIndents
630                 null,  // rightIndents
631                 JUSTIFICATION_MODE_NONE,
632                 null,  // lineBreakConfig,
633                 false,  // useBoundsForWidth
634                 false,  // shiftDrawingOffsetForStartOverhang
635                 null  // minimumFontMetrics
636         );
637 
638         mColumns = COLUMNS_ELLIPSIZE;
639         mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2);
640         mLines  = ArrayUtils.newUnpaddedIntArray(2 * mColumns);
641     }
642 
643     /**
644      * @deprecated Use {@link Builder} instead.
645      */
646     @Deprecated
StaticLayout(CharSequence source, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad)647     public StaticLayout(CharSequence source, TextPaint paint,
648                         int width,
649                         Alignment align, float spacingmult, float spacingadd,
650                         boolean includepad) {
651         this(source, 0, source.length(), paint, width, align,
652              spacingmult, spacingadd, includepad);
653     }
654 
655     /**
656      * @deprecated Use {@link Builder} instead.
657      */
658     @Deprecated
StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, boolean includepad)659     public StaticLayout(CharSequence source, int bufstart, int bufend,
660                         TextPaint paint, int outerwidth,
661                         Alignment align,
662                         float spacingmult, float spacingadd,
663                         boolean includepad) {
664         this(source, bufstart, bufend, paint, outerwidth, align,
665              spacingmult, spacingadd, includepad, null, 0);
666     }
667 
668     /**
669      * @deprecated Use {@link Builder} instead.
670      */
671     @Deprecated
StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)672     public StaticLayout(CharSequence source, int bufstart, int bufend,
673             TextPaint paint, int outerwidth,
674             Alignment align,
675             float spacingmult, float spacingadd,
676             boolean includepad,
677             TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
678         this(source, bufstart, bufend, paint, outerwidth, align,
679                 TextDirectionHeuristics.FIRSTSTRONG_LTR,
680                 spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
681     }
682 
683     /**
684      * @hide
685      * @deprecated Use {@link Builder} instead.
686      */
687     @Deprecated
688     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 117521430)
StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, TextDirectionHeuristic textDir, float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines)689     public StaticLayout(CharSequence source, int bufstart, int bufend,
690                         TextPaint paint, int outerwidth,
691                         Alignment align, TextDirectionHeuristic textDir,
692                         float spacingmult, float spacingadd,
693                         boolean includepad,
694                         TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) {
695         this(Builder.obtain(source, bufstart, bufend, paint, outerwidth)
696                 .setAlignment(align)
697                 .setTextDirection(textDir)
698                 .setLineSpacing(spacingadd, spacingmult)
699                 .setIncludePad(includepad)
700                 .setEllipsize(ellipsize)
701                 .setEllipsizedWidth(ellipsizedWidth)
702                 .setMaxLines(maxLines), includepad,
703                 ellipsize != null ? COLUMNS_ELLIPSIZE : COLUMNS_NORMAL);
704     }
705 
StaticLayout(Builder b, boolean trackPadding, int columnSize)706     private StaticLayout(Builder b, boolean trackPadding, int columnSize) {
707         super((b.mEllipsize == null) ? b.mText : (b.mText instanceof Spanned)
708                     ? new SpannedEllipsizer(b.mText) : new Ellipsizer(b.mText),
709                 b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd,
710                 b.mIncludePad, b.mFallbackLineSpacing, b.mEllipsizedWidth, b.mEllipsize,
711                 b.mMaxLines, b.mBreakStrategy, b.mHyphenationFrequency, b.mLeftIndents,
712                 b.mRightIndents, b.mJustificationMode, b.mLineBreakConfig, b.mUseBoundsForWidth,
713                 b.mShiftDrawingOffsetForStartOverhang, b.mMinimumFontMetrics);
714 
715         mColumns = columnSize;
716         if (b.mEllipsize != null) {
717             Ellipsizer e = (Ellipsizer) getText();
718 
719             e.mLayout = this;
720             e.mWidth = b.mEllipsizedWidth;
721             e.mMethod = b.mEllipsize;
722         }
723 
724         mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2);
725         mLines  = ArrayUtils.newUnpaddedIntArray(2 * mColumns);
726         mMaximumVisibleLineCount = b.mMaxLines;
727 
728         mLeftIndents = b.mLeftIndents;
729         mRightIndents = b.mRightIndents;
730 
731         Trace.beginSection("Constructing StaticLayout");
732         try {
733             generate(b, b.mIncludePad, trackPadding);
734         } finally {
735             Trace.endSection();
736         }
737     }
738 
getBaseHyphenationFrequency(int frequency)739     private static int getBaseHyphenationFrequency(int frequency) {
740         switch (frequency) {
741             case Layout.HYPHENATION_FREQUENCY_FULL:
742             case Layout.HYPHENATION_FREQUENCY_FULL_FAST:
743                 return LineBreaker.HYPHENATION_FREQUENCY_FULL;
744             case Layout.HYPHENATION_FREQUENCY_NORMAL:
745             case Layout.HYPHENATION_FREQUENCY_NORMAL_FAST:
746                 return LineBreaker.HYPHENATION_FREQUENCY_NORMAL;
747             case Layout.HYPHENATION_FREQUENCY_NONE:
748             default:
749                 return LineBreaker.HYPHENATION_FREQUENCY_NONE;
750         }
751     }
752 
generate(Builder b, boolean includepad, boolean trackpad)753     /* package */ void generate(Builder b, boolean includepad, boolean trackpad) {
754         final CharSequence source = b.mText;
755         final int bufStart = b.mStart;
756         final int bufEnd = b.mEnd;
757         TextPaint paint = b.mPaint;
758         int outerWidth = b.mWidth;
759         TextDirectionHeuristic textDir = b.mTextDir;
760         float spacingmult = b.mSpacingMult;
761         float spacingadd = b.mSpacingAdd;
762         float ellipsizedWidth = b.mEllipsizedWidth;
763         TextUtils.TruncateAt ellipsize = b.mEllipsize;
764         final boolean addLastLineSpacing = b.mAddLastLineLineSpacing;
765 
766         int lineBreakCapacity = 0;
767         int[] breaks = null;
768         float[] lineWidths = null;
769         float[] ascents = null;
770         float[] descents = null;
771         boolean[] hasTabs = null;
772         int[] hyphenEdits = null;
773 
774         mLineCount = 0;
775         mEllipsized = false;
776         mMaxLineHeight = mMaximumVisibleLineCount < 1 ? 0 : DEFAULT_MAX_LINE_HEIGHT;
777         mDrawingBounds = null;
778         boolean isFallbackLineSpacing = b.mFallbackLineSpacing;
779 
780         int v = 0;
781         boolean needMultiply = (spacingmult != 1 || spacingadd != 0);
782 
783         Paint.FontMetricsInt fm = b.mFontMetricsInt;
784         int[] chooseHtv = null;
785 
786         final int[] indents;
787         if (mLeftIndents != null || mRightIndents != null) {
788             final int leftLen = mLeftIndents == null ? 0 : mLeftIndents.length;
789             final int rightLen = mRightIndents == null ? 0 : mRightIndents.length;
790             final int indentsLen = Math.max(leftLen, rightLen);
791             indents = new int[indentsLen];
792             for (int i = 0; i < leftLen; i++) {
793                 indents[i] = mLeftIndents[i];
794             }
795             for (int i = 0; i < rightLen; i++) {
796                 indents[i] += mRightIndents[i];
797             }
798         } else {
799             indents = null;
800         }
801 
802         int defaultTop;
803         final int defaultAscent;
804         final int defaultDescent;
805         int defaultBottom;
806         if (ClientFlags.fixLineHeightForLocale() && b.mMinimumFontMetrics != null) {
807             defaultTop = (int) Math.floor(b.mMinimumFontMetrics.top);
808             defaultAscent = Math.round(b.mMinimumFontMetrics.ascent);
809             defaultDescent = Math.round(b.mMinimumFontMetrics.descent);
810             defaultBottom = (int) Math.ceil(b.mMinimumFontMetrics.bottom);
811 
812             // Because the font metrics is provided by public APIs, adjust the top/bottom with
813             // ascent/descent: top must be smaller than ascent, bottom must be larger than descent.
814             defaultTop = Math.min(defaultTop, defaultAscent);
815             defaultBottom = Math.max(defaultBottom, defaultDescent);
816         } else {
817             defaultTop = 0;
818             defaultAscent = 0;
819             defaultDescent = 0;
820             defaultBottom = 0;
821         }
822 
823         final LineBreaker lineBreaker = new LineBreaker.Builder()
824                 .setBreakStrategy(b.mBreakStrategy)
825                 .setHyphenationFrequency(getBaseHyphenationFrequency(b.mHyphenationFrequency))
826                 // TODO: Support more justification mode, e.g. letter spacing, stretching.
827                 .setJustificationMode(b.mJustificationMode)
828                 .setIndents(indents)
829                 .setUseBoundsForWidth(b.mUseBoundsForWidth)
830                 .build();
831 
832         LineBreaker.ParagraphConstraints constraints =
833                 new LineBreaker.ParagraphConstraints();
834 
835         PrecomputedText.ParagraphInfo[] paragraphInfo = null;
836         final Spanned spanned = (source instanceof Spanned) ? (Spanned) source : null;
837         if (source instanceof PrecomputedText) {
838             PrecomputedText precomputed = (PrecomputedText) source;
839             final @PrecomputedText.Params.CheckResultUsableResult int checkResult =
840                     precomputed.checkResultUsable(bufStart, bufEnd, textDir, paint,
841                             b.mBreakStrategy, b.mHyphenationFrequency, b.mLineBreakConfig);
842             switch (checkResult) {
843                 case PrecomputedText.Params.UNUSABLE:
844                     break;
845                 case PrecomputedText.Params.NEED_RECOMPUTE:
846                     final PrecomputedText.Params newParams =
847                             new PrecomputedText.Params.Builder(paint)
848                                 .setBreakStrategy(b.mBreakStrategy)
849                                 .setHyphenationFrequency(b.mHyphenationFrequency)
850                                 .setTextDirection(textDir)
851                                 .setLineBreakConfig(b.mLineBreakConfig)
852                                 .build();
853                     precomputed = PrecomputedText.create(precomputed, newParams);
854                     paragraphInfo = precomputed.getParagraphInfo();
855                     break;
856                 case PrecomputedText.Params.USABLE:
857                     // Some parameters are different from the ones when measured text is created.
858                     paragraphInfo = precomputed.getParagraphInfo();
859                     break;
860             }
861         }
862 
863         if (paragraphInfo == null) {
864             final PrecomputedText.Params param = new PrecomputedText.Params(paint,
865                     b.mLineBreakConfig, textDir, b.mBreakStrategy, b.mHyphenationFrequency);
866             paragraphInfo = PrecomputedText.createMeasuredParagraphs(source, param, bufStart,
867                     bufEnd, false /* computeLayout */, b.mCalculateBounds);
868         }
869 
870         for (int paraIndex = 0; paraIndex < paragraphInfo.length; paraIndex++) {
871             final int paraStart = paraIndex == 0
872                     ? bufStart : paragraphInfo[paraIndex - 1].paragraphEnd;
873             final int paraEnd = paragraphInfo[paraIndex].paragraphEnd;
874 
875             int firstWidthLineCount = 1;
876             int firstWidth = outerWidth;
877             int restWidth = outerWidth;
878 
879             LineHeightSpan[] chooseHt = null;
880             if (spanned != null) {
881                 LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd,
882                         LeadingMarginSpan.class);
883                 for (int i = 0; i < sp.length; i++) {
884                     LeadingMarginSpan lms = sp[i];
885                     firstWidth -= sp[i].getLeadingMargin(true);
886                     restWidth -= sp[i].getLeadingMargin(false);
887 
888                     // LeadingMarginSpan2 is odd.  The count affects all
889                     // leading margin spans, not just this particular one
890                     if (lms instanceof LeadingMarginSpan2) {
891                         LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms;
892                         firstWidthLineCount = Math.max(firstWidthLineCount,
893                                 lms2.getLeadingMarginLineCount());
894                     }
895                 }
896 
897                 chooseHt = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class);
898 
899                 if (chooseHt.length == 0) {
900                     chooseHt = null; // So that out() would not assume it has any contents
901                 } else {
902                     if (chooseHtv == null || chooseHtv.length < chooseHt.length) {
903                         chooseHtv = ArrayUtils.newUnpaddedIntArray(chooseHt.length);
904                     }
905 
906                     for (int i = 0; i < chooseHt.length; i++) {
907                         int o = spanned.getSpanStart(chooseHt[i]);
908 
909                         if (o < paraStart) {
910                             // starts in this layout, before the
911                             // current paragraph
912 
913                             chooseHtv[i] = getLineTop(getLineForOffset(o));
914                         } else {
915                             // starts in this paragraph
916 
917                             chooseHtv[i] = v;
918                         }
919                     }
920                 }
921             }
922             // tab stop locations
923             float[] variableTabStops = null;
924             if (spanned != null) {
925                 TabStopSpan[] spans = getParagraphSpans(spanned, paraStart,
926                         paraEnd, TabStopSpan.class);
927                 if (spans.length > 0) {
928                     float[] stops = new float[spans.length];
929                     for (int i = 0; i < spans.length; i++) {
930                         stops[i] = (float) spans[i].getTabStop();
931                     }
932                     Arrays.sort(stops, 0, stops.length);
933                     variableTabStops = stops;
934                 }
935             }
936 
937             final MeasuredParagraph measuredPara = paragraphInfo[paraIndex].measured;
938             final char[] chs = measuredPara.getChars();
939             final int[] spanEndCache = measuredPara.getSpanEndCache().getRawArray();
940             final int[] fmCache = measuredPara.getFontMetrics().getRawArray();
941 
942             constraints.setWidth(restWidth);
943             constraints.setIndent(firstWidth, firstWidthLineCount);
944             constraints.setTabStops(variableTabStops, TAB_INCREMENT);
945 
946             LineBreaker.Result res = lineBreaker.computeLineBreaks(
947                     measuredPara.getMeasuredText(), constraints, mLineCount);
948             int breakCount = res.getLineCount();
949             if (lineBreakCapacity < breakCount) {
950                 lineBreakCapacity = breakCount;
951                 breaks = new int[lineBreakCapacity];
952                 lineWidths = new float[lineBreakCapacity];
953                 ascents = new float[lineBreakCapacity];
954                 descents = new float[lineBreakCapacity];
955                 hasTabs = new boolean[lineBreakCapacity];
956                 hyphenEdits = new int[lineBreakCapacity];
957             }
958 
959             for (int i = 0; i < breakCount; ++i) {
960                 breaks[i] = res.getLineBreakOffset(i);
961                 lineWidths[i] = res.getLineWidth(i);
962                 ascents[i] = res.getLineAscent(i);
963                 descents[i] = res.getLineDescent(i);
964                 hasTabs[i] = res.hasLineTab(i);
965                 hyphenEdits[i] =
966                     packHyphenEdit(res.getStartLineHyphenEdit(i), res.getEndLineHyphenEdit(i));
967             }
968 
969             final int remainingLineCount = mMaximumVisibleLineCount - mLineCount;
970             final boolean ellipsisMayBeApplied = ellipsize != null
971                     && (ellipsize == TextUtils.TruncateAt.END
972                         || (mMaximumVisibleLineCount == 1
973                                 && ellipsize != TextUtils.TruncateAt.MARQUEE));
974             if (0 < remainingLineCount && remainingLineCount < breakCount
975                     && ellipsisMayBeApplied) {
976                 // Calculate width
977                 float width = 0;
978                 boolean hasTab = false;  // XXX May need to also have starting hyphen edit
979                 for (int i = remainingLineCount - 1; i < breakCount; i++) {
980                     if (i == breakCount - 1) {
981                         width += lineWidths[i];
982                     } else {
983                         for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) {
984                             width += measuredPara.getCharWidthAt(j);
985                         }
986                     }
987                     hasTab |= hasTabs[i];
988                 }
989                 // Treat the last line and overflowed lines as a single line.
990                 breaks[remainingLineCount - 1] = breaks[breakCount - 1];
991                 lineWidths[remainingLineCount - 1] = width;
992                 hasTabs[remainingLineCount - 1] = hasTab;
993 
994                 breakCount = remainingLineCount;
995             }
996 
997             // here is the offset of the starting character of the line we are currently
998             // measuring
999             int here = paraStart;
1000 
1001             int fmTop = defaultTop;
1002             int fmBottom = defaultBottom;
1003             int fmAscent = defaultAscent;
1004             int fmDescent = defaultDescent;
1005             int fmCacheIndex = 0;
1006             int spanEndCacheIndex = 0;
1007             int breakIndex = 0;
1008             for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
1009                 // retrieve end of span
1010                 spanEnd = spanEndCache[spanEndCacheIndex++];
1011 
1012                 // retrieve cached metrics, order matches above
1013                 fm.top = fmCache[fmCacheIndex * 4 + 0];
1014                 fm.bottom = fmCache[fmCacheIndex * 4 + 1];
1015                 fm.ascent = fmCache[fmCacheIndex * 4 + 2];
1016                 fm.descent = fmCache[fmCacheIndex * 4 + 3];
1017                 fmCacheIndex++;
1018 
1019                 if (fm.top < fmTop) {
1020                     fmTop = fm.top;
1021                 }
1022                 if (fm.ascent < fmAscent) {
1023                     fmAscent = fm.ascent;
1024                 }
1025                 if (fm.descent > fmDescent) {
1026                     fmDescent = fm.descent;
1027                 }
1028                 if (fm.bottom > fmBottom) {
1029                     fmBottom = fm.bottom;
1030                 }
1031 
1032                 // skip breaks ending before current span range
1033                 while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) {
1034                     breakIndex++;
1035                 }
1036 
1037                 while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) {
1038                     int endPos = paraStart + breaks[breakIndex];
1039 
1040                     boolean moreChars = (endPos < bufEnd);
1041 
1042                     final int ascent = isFallbackLineSpacing
1043                             ? Math.min(fmAscent, Math.round(ascents[breakIndex]))
1044                             : fmAscent;
1045                     final int descent = isFallbackLineSpacing
1046                             ? Math.max(fmDescent, Math.round(descents[breakIndex]))
1047                             : fmDescent;
1048 
1049                     // The fallback ascent/descent may be larger than top/bottom of the default font
1050                     // metrics. Adjust top/bottom with ascent/descent for avoiding unexpected
1051                     // clipping.
1052                     if (isFallbackLineSpacing) {
1053                         if (ascent < fmTop) {
1054                             fmTop = ascent;
1055                         }
1056                         if (descent > fmBottom) {
1057                             fmBottom = descent;
1058                         }
1059                     }
1060 
1061                     v = out(source, here, endPos,
1062                             ascent, descent, fmTop, fmBottom,
1063                             v, spacingmult, spacingadd, chooseHt, chooseHtv, fm,
1064                             hasTabs[breakIndex], hyphenEdits[breakIndex], needMultiply,
1065                             measuredPara, bufEnd, includepad, trackpad, addLastLineSpacing, chs,
1066                             paraStart, ellipsize, ellipsizedWidth, lineWidths[breakIndex],
1067                             paint, moreChars);
1068 
1069                     if (endPos < spanEnd) {
1070                         // preserve metrics for current span
1071                         fmTop = Math.min(defaultTop, fm.top);
1072                         fmBottom = Math.max(defaultBottom, fm.bottom);
1073                         fmAscent = Math.min(defaultAscent, fm.ascent);
1074                         fmDescent = Math.max(defaultDescent, fm.descent);
1075                     } else {
1076                         fmTop = fmBottom = fmAscent = fmDescent = 0;
1077                     }
1078 
1079                     here = endPos;
1080                     breakIndex++;
1081 
1082                     if (mLineCount >= mMaximumVisibleLineCount && mEllipsized) {
1083                         return;
1084                     }
1085                 }
1086             }
1087 
1088             if (paraEnd == bufEnd) {
1089                 break;
1090             }
1091         }
1092 
1093         if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE)
1094                 && mLineCount < mMaximumVisibleLineCount) {
1095             final MeasuredParagraph measuredPara =
1096                     MeasuredParagraph.buildForBidi(source, bufEnd, bufEnd, textDir, null);
1097             if (defaultAscent != 0 && defaultDescent != 0) {
1098                 fm.top = defaultTop;
1099                 fm.ascent = defaultAscent;
1100                 fm.descent = defaultDescent;
1101                 fm.bottom = defaultBottom;
1102             } else {
1103                 paint.getFontMetricsInt(fm);
1104             }
1105 
1106             v = out(source,
1107                     bufEnd, bufEnd, fm.ascent, fm.descent,
1108                     fm.top, fm.bottom,
1109                     v,
1110                     spacingmult, spacingadd, null,
1111                     null, fm, false, 0,
1112                     needMultiply, measuredPara, bufEnd,
1113                     includepad, trackpad, addLastLineSpacing, null,
1114                     bufStart, ellipsize,
1115                     ellipsizedWidth, 0, paint, false);
1116         }
1117     }
1118 
1119     private int out(final CharSequence text, final int start, final int end, int above, int below,
1120             int top, int bottom, int v, final float spacingmult, final float spacingadd,
1121             final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm,
1122             final boolean hasTab, final int hyphenEdit, final boolean needMultiply,
1123             @NonNull final MeasuredParagraph measured,
1124             final int bufEnd, final boolean includePad, final boolean trackPad,
1125             final boolean addLastLineLineSpacing, final char[] chs,
1126             final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth,
1127             final float textWidth, final TextPaint paint, final boolean moreChars) {
1128         final int j = mLineCount;
1129         final int off = j * mColumns;
1130         final int want = off + mColumns + TOP;
1131         int[] lines = mLines;
1132         final int dir = measured.getParagraphDir();
1133 
1134         if (want >= lines.length) {
1135             final int[] grow = ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(want));
1136             System.arraycopy(lines, 0, grow, 0, lines.length);
1137             mLines = grow;
1138             lines = grow;
1139         }
1140 
1141         if (j >= mLineDirections.length) {
1142             final Directions[] grow = ArrayUtils.newUnpaddedArray(Directions.class,
1143                     GrowingArrayUtils.growSize(j));
1144             System.arraycopy(mLineDirections, 0, grow, 0, mLineDirections.length);
1145             mLineDirections = grow;
1146         }
1147 
1148         if (chooseHt != null) {
1149             fm.ascent = above;
1150             fm.descent = below;
1151             fm.top = top;
1152             fm.bottom = bottom;
1153 
1154             for (int i = 0; i < chooseHt.length; i++) {
1155                 if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
1156                     ((LineHeightSpan.WithDensity) chooseHt[i])
1157                             .chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
1158                 } else {
1159                     chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
1160                 }
1161             }
1162 
1163             above = fm.ascent;
1164             below = fm.descent;
1165             top = fm.top;
1166             bottom = fm.bottom;
1167         }
1168 
1169         boolean firstLine = (j == 0);
1170         boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
1171 
1172         if (ellipsize != null) {
1173             // If there is only one line, then do any type of ellipsis except when it is MARQUEE
1174             // if there are multiple lines, just allow END ellipsis on the last line
1175             boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
1176 
1177             boolean doEllipsis =
1178                     (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) &&
1179                             ellipsize != TextUtils.TruncateAt.MARQUEE) ||
1180                     (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
1181                             ellipsize == TextUtils.TruncateAt.END);
1182             if (doEllipsis) {
1183                 calculateEllipsis(start, end, measured, widthStart,
1184                         ellipsisWidth, ellipsize, j,
1185                         textWidth, paint, forceEllipsis);
1186             } else {
1187                 mLines[mColumns * j + ELLIPSIS_START] = 0;
1188                 mLines[mColumns * j + ELLIPSIS_COUNT] = 0;
1189             }
1190         }
1191 
1192         final boolean lastLine;
1193         if (mEllipsized) {
1194             lastLine = true;
1195         } else {
1196             final boolean lastCharIsNewLine = widthStart != bufEnd && bufEnd > 0
1197                     && text.charAt(bufEnd - 1) == CHAR_NEW_LINE;
1198             if (end == bufEnd && !lastCharIsNewLine) {
1199                 lastLine = true;
1200             } else if (start == bufEnd && lastCharIsNewLine) {
1201                 lastLine = true;
1202             } else {
1203                 lastLine = false;
1204             }
1205         }
1206 
1207         if (firstLine) {
1208             if (trackPad) {
1209                 mTopPadding = top - above;
1210             }
1211 
1212             if (includePad) {
1213                 above = top;
1214             }
1215         }
1216 
1217         int extra;
1218 
1219         if (lastLine) {
1220             if (trackPad) {
1221                 mBottomPadding = bottom - below;
1222             }
1223 
1224             if (includePad) {
1225                 below = bottom;
1226             }
1227         }
1228 
1229         if (needMultiply && (addLastLineLineSpacing || !lastLine)) {
1230             double ex = (below - above) * (spacingmult - 1) + spacingadd;
1231             if (ex >= 0) {
1232                 extra = (int)(ex + EXTRA_ROUNDING);
1233             } else {
1234                 extra = -(int)(-ex + EXTRA_ROUNDING);
1235             }
1236         } else {
1237             extra = 0;
1238         }
1239 
1240         lines[off + START] = start;
1241         lines[off + TOP] = v;
1242         lines[off + DESCENT] = below + extra;
1243         lines[off + EXTRA] = extra;
1244 
1245         // special case for non-ellipsized last visible line when maxLines is set
1246         // store the height as if it was ellipsized
1247         if (!mEllipsized && currentLineIsTheLastVisibleOne) {
1248             // below calculation as if it was the last line
1249             int maxLineBelow = includePad ? bottom : below;
1250             // similar to the calculation of v below, without the extra.
1251             mMaxLineHeight = v + (maxLineBelow - above);
1252         }
1253 
1254         v += (below - above) + extra;
1255         lines[off + mColumns + START] = end;
1256         lines[off + mColumns + TOP] = v;
1257 
1258         // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining
1259         // one bit for start field
1260         lines[off + TAB] |= hasTab ? TAB_MASK : 0;
1261         if (mEllipsized) {
1262             if (ellipsize == TextUtils.TruncateAt.START) {
1263                 lines[off + HYPHEN] = packHyphenEdit(Paint.START_HYPHEN_EDIT_NO_EDIT,
1264                         unpackEndHyphenEdit(hyphenEdit));
1265             } else if (ellipsize == TextUtils.TruncateAt.END) {
1266                 lines[off + HYPHEN] = packHyphenEdit(unpackStartHyphenEdit(hyphenEdit),
1267                         Paint.END_HYPHEN_EDIT_NO_EDIT);
1268             } else {  // Middle and marquee ellipsize should show text at the start/end edge.
1269                 lines[off + HYPHEN] = packHyphenEdit(
1270                         Paint.START_HYPHEN_EDIT_NO_EDIT, Paint.END_HYPHEN_EDIT_NO_EDIT);
1271             }
1272         } else {
1273             lines[off + HYPHEN] = hyphenEdit;
1274         }
1275 
1276         lines[off + DIR] |= dir << DIR_SHIFT;
1277         mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart);
1278 
1279         mLineCount++;
1280         return v;
1281     }
1282 
1283     private void calculateEllipsis(int lineStart, int lineEnd,
1284                                    MeasuredParagraph measured, int widthStart,
1285                                    float avail, TextUtils.TruncateAt where,
1286                                    int line, float textWidth, TextPaint paint,
1287                                    boolean forceEllipsis) {
1288         avail -= getTotalInsets(line);
1289         if (textWidth <= avail && !forceEllipsis) {
1290             // Everything fits!
1291             mLines[mColumns * line + ELLIPSIS_START] = 0;
1292             mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
1293             return;
1294         }
1295 
1296         float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where));
1297         int ellipsisStart = 0;
1298         int ellipsisCount = 0;
1299         int len = lineEnd - lineStart;
1300 
1301         // We only support start ellipsis on a single line
1302         if (where == TextUtils.TruncateAt.START) {
1303             if (mMaximumVisibleLineCount == 1) {
1304                 float sum = 0;
1305                 int i;
1306 
1307                 for (i = len; i > 0; i--) {
1308                     float w = measured.getCharWidthAt(i - 1 + lineStart - widthStart);
1309                     if (w + sum + ellipsisWidth > avail) {
1310                         while (i < len
1311                                 && measured.getCharWidthAt(i + lineStart - widthStart) == 0.0f) {
1312                             i++;
1313                         }
1314                         break;
1315                     }
1316 
1317                     sum += w;
1318                 }
1319 
1320                 ellipsisStart = 0;
1321                 ellipsisCount = i;
1322             } else {
1323                 if (Log.isLoggable(TAG, Log.WARN)) {
1324                     Log.w(TAG, "Start Ellipsis only supported with one line");
1325                 }
1326             }
1327         } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
1328                 where == TextUtils.TruncateAt.END_SMALL) {
1329             float sum = 0;
1330             int i;
1331 
1332             for (i = 0; i < len; i++) {
1333                 float w = measured.getCharWidthAt(i + lineStart - widthStart);
1334 
1335                 if (w + sum + ellipsisWidth > avail) {
1336                     break;
1337                 }
1338 
1339                 sum += w;
1340             }
1341 
1342             ellipsisStart = i;
1343             ellipsisCount = len - i;
1344             if (forceEllipsis && ellipsisCount == 0 && len > 0) {
1345                 ellipsisStart = len - 1;
1346                 ellipsisCount = 1;
1347             }
1348         } else {
1349             // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line
1350             if (mMaximumVisibleLineCount == 1) {
1351                 float lsum = 0, rsum = 0;
1352                 int left = 0, right = len;
1353 
1354                 float ravail = (avail - ellipsisWidth) / 2;
1355                 for (right = len; right > 0; right--) {
1356                     float w = measured.getCharWidthAt(right - 1 + lineStart - widthStart);
1357 
1358                     if (w + rsum > ravail) {
1359                         while (right < len
1360                                 && measured.getCharWidthAt(right + lineStart - widthStart)
1361                                     == 0.0f) {
1362                             right++;
1363                         }
1364                         break;
1365                     }
1366                     rsum += w;
1367                 }
1368 
1369                 float lavail = avail - ellipsisWidth - rsum;
1370                 for (left = 0; left < right; left++) {
1371                     float w = measured.getCharWidthAt(left + lineStart - widthStart);
1372 
1373                     if (w + lsum > lavail) {
1374                         break;
1375                     }
1376 
1377                     lsum += w;
1378                 }
1379 
1380                 ellipsisStart = left;
1381                 ellipsisCount = right - left;
1382             } else {
1383                 if (Log.isLoggable(TAG, Log.WARN)) {
1384                     Log.w(TAG, "Middle Ellipsis only supported with one line");
1385                 }
1386             }
1387         }
1388         mEllipsized = true;
1389         mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
1390         mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
1391     }
1392 
1393     private float getTotalInsets(int line) {
1394         int totalIndent = 0;
1395         if (mLeftIndents != null) {
1396             totalIndent = mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
1397         }
1398         if (mRightIndents != null) {
1399             totalIndent += mRightIndents[Math.min(line, mRightIndents.length - 1)];
1400         }
1401         return totalIndent;
1402     }
1403 
1404     // Override the base class so we can directly access our members,
1405     // rather than relying on member functions.
1406     // The logic mirrors that of Layout.getLineForVertical
1407     // FIXME: It may be faster to do a linear search for layouts without many lines.
1408     @Override
1409     public int getLineForVertical(int vertical) {
1410         int high = mLineCount;
1411         int low = -1;
1412         int guess;
1413         int[] lines = mLines;
1414         while (high - low > 1) {
1415             guess = (high + low) >> 1;
1416             if (lines[mColumns * guess + TOP] > vertical){
1417                 high = guess;
1418             } else {
1419                 low = guess;
1420             }
1421         }
1422         if (low < 0) {
1423             return 0;
1424         } else {
1425             return low;
1426         }
1427     }
1428 
1429     @Override
1430     public int getLineCount() {
1431         return mLineCount;
1432     }
1433 
1434     @Override
1435     public int getLineTop(int line) {
1436         return mLines[mColumns * line + TOP];
1437     }
1438 
1439     /**
1440      * @hide
1441      */
1442     @Override
1443     public int getLineExtra(int line) {
1444         return mLines[mColumns * line + EXTRA];
1445     }
1446 
1447     @Override
1448     public int getLineDescent(int line) {
1449         return mLines[mColumns * line + DESCENT];
1450     }
1451 
1452     @Override
1453     public int getLineStart(int line) {
1454         return mLines[mColumns * line + START] & START_MASK;
1455     }
1456 
1457     @Override
1458     public int getParagraphDirection(int line) {
1459         return mLines[mColumns * line + DIR] >> DIR_SHIFT;
1460     }
1461 
1462     @Override
1463     public boolean getLineContainsTab(int line) {
1464         return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
1465     }
1466 
1467     @Override
1468     public final Directions getLineDirections(int line) {
1469         if (line > getLineCount()) {
1470             throw new ArrayIndexOutOfBoundsException();
1471         }
1472         return mLineDirections[line];
1473     }
1474 
1475     @Override
1476     public int getTopPadding() {
1477         return mTopPadding;
1478     }
1479 
1480     @Override
1481     public int getBottomPadding() {
1482         return mBottomPadding;
1483     }
1484 
1485     // To store into single int field, pack the pair of start and end hyphen edit.
1486     static int packHyphenEdit(
1487             @Paint.StartHyphenEdit int start, @Paint.EndHyphenEdit int end) {
1488         return start << START_HYPHEN_BITS_SHIFT | end;
1489     }
1490 
1491     static int unpackStartHyphenEdit(int packedHyphenEdit) {
1492         return (packedHyphenEdit & START_HYPHEN_MASK) >> START_HYPHEN_BITS_SHIFT;
1493     }
1494 
1495     static int unpackEndHyphenEdit(int packedHyphenEdit) {
1496         return packedHyphenEdit & END_HYPHEN_MASK;
1497     }
1498 
1499     /**
1500      * Returns the start hyphen edit value for this line.
1501      *
1502      * @param lineNumber a line number
1503      * @return A start hyphen edit value.
1504      * @hide
1505      */
1506     @Override
1507     public @Paint.StartHyphenEdit int getStartHyphenEdit(int lineNumber) {
1508         return unpackStartHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK);
1509     }
1510 
1511     /**
1512      * Returns the packed hyphen edit value for this line.
1513      *
1514      * @param lineNumber a line number
1515      * @return An end hyphen edit value.
1516      * @hide
1517      */
1518     @Override
1519     public @Paint.EndHyphenEdit int getEndHyphenEdit(int lineNumber) {
1520         return unpackEndHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK);
1521     }
1522 
1523     /**
1524      * @hide
1525      */
1526     @Override
1527     public int getIndentAdjust(int line, Alignment align) {
1528         if (align == Alignment.ALIGN_LEFT) {
1529             if (mLeftIndents == null) {
1530                 return 0;
1531             } else {
1532                 return mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
1533             }
1534         } else if (align == Alignment.ALIGN_RIGHT) {
1535             if (mRightIndents == null) {
1536                 return 0;
1537             } else {
1538                 return -mRightIndents[Math.min(line, mRightIndents.length - 1)];
1539             }
1540         } else if (align == Alignment.ALIGN_CENTER) {
1541             int left = 0;
1542             if (mLeftIndents != null) {
1543                 left = mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
1544             }
1545             int right = 0;
1546             if (mRightIndents != null) {
1547                 right = mRightIndents[Math.min(line, mRightIndents.length - 1)];
1548             }
1549             return (left - right) >> 1;
1550         } else {
1551             throw new AssertionError("unhandled alignment " + align);
1552         }
1553     }
1554 
1555     @Override
1556     public int getEllipsisCount(int line) {
1557         if (mColumns < COLUMNS_ELLIPSIZE) {
1558             return 0;
1559         }
1560 
1561         return mLines[mColumns * line + ELLIPSIS_COUNT];
1562     }
1563 
1564     @Override
1565     public int getEllipsisStart(int line) {
1566         if (mColumns < COLUMNS_ELLIPSIZE) {
1567             return 0;
1568         }
1569 
1570         return mLines[mColumns * line + ELLIPSIS_START];
1571     }
1572 
1573     @Override
1574     @NonNull
1575     public RectF computeDrawingBoundingBox() {
1576         // Cache the drawing bounds result because it does not change after created.
1577         if (mDrawingBounds == null) {
1578             mDrawingBounds = super.computeDrawingBoundingBox();
1579         }
1580         return mDrawingBounds;
1581     }
1582 
1583     /**
1584      * Return the total height of this layout.
1585      *
1586      * @param cap if true and max lines is set, returns the height of the layout at the max lines.
1587      *
1588      * @hide
1589      */
1590     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
1591     public int getHeight(boolean cap) {
1592         if (cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight == -1
1593                 && Log.isLoggable(TAG, Log.WARN)) {
1594             Log.w(TAG, "maxLineHeight should not be -1. "
1595                     + " maxLines:" + mMaximumVisibleLineCount
1596                     + " lineCount:" + mLineCount);
1597         }
1598 
1599         return cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight != -1
1600                 ? mMaxLineHeight : super.getHeight();
1601     }
1602 
1603     @UnsupportedAppUsage
1604     private int mLineCount;
1605     private int mTopPadding, mBottomPadding;
1606     @UnsupportedAppUsage
1607     private int mColumns;
1608     private RectF mDrawingBounds = null;  // lazy calculation.
1609 
1610     /**
1611      * Keeps track if ellipsize is applied to the text.
1612      */
1613     private boolean mEllipsized;
1614 
1615     /**
1616      * If maxLines is set, ellipsize is not set, and the actual line count of text is greater than
1617      * or equal to maxLine, this variable holds the ideal visual height of the maxLine'th line
1618      * starting from the top of the layout. If maxLines is not set its value will be -1.
1619      *
1620      * The value is the same as getLineTop(maxLines) for ellipsized version where structurally no
1621      * more than maxLines is contained.
1622      */
1623     private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT;
1624 
1625     private static final int COLUMNS_NORMAL = 5;
1626     private static final int COLUMNS_ELLIPSIZE = 7;
1627     private static final int START = 0;
1628     private static final int DIR = START;
1629     private static final int TAB = START;
1630     private static final int TOP = 1;
1631     private static final int DESCENT = 2;
1632     private static final int EXTRA = 3;
1633     private static final int HYPHEN = 4;
1634     @UnsupportedAppUsage
1635     private static final int ELLIPSIS_START = 5;
1636     private static final int ELLIPSIS_COUNT = 6;
1637 
1638     @UnsupportedAppUsage
1639     private int[] mLines;
1640     @UnsupportedAppUsage
1641     private Directions[] mLineDirections;
1642     @UnsupportedAppUsage
1643     private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
1644 
1645     private static final int START_MASK = 0x1FFFFFFF;
1646     private static final int DIR_SHIFT  = 30;
1647     private static final int TAB_MASK   = 0x20000000;
1648     private static final int HYPHEN_MASK = 0xFF;
1649     private static final int START_HYPHEN_BITS_SHIFT = 3;
1650     private static final int START_HYPHEN_MASK = 0x18; // 0b11000
1651     private static final int END_HYPHEN_MASK = 0x7;  // 0b00111
1652 
1653     private static final float TAB_INCREMENT = 20; // same as Layout, but that's private
1654 
1655     private static final char CHAR_NEW_LINE = '\n';
1656 
1657     private static final double EXTRA_ROUNDING = 0.5;
1658 
1659     private static final int DEFAULT_MAX_LINE_HEIGHT = -1;
1660 
1661     // Unused, here because of gray list private API accesses.
1662     /*package*/ static class LineBreaks {
1663         private static final int INITIAL_SIZE = 16;
1664         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
1665         public int[] breaks = new int[INITIAL_SIZE];
1666         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
1667         public float[] widths = new float[INITIAL_SIZE];
1668         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
1669         public float[] ascents = new float[INITIAL_SIZE];
1670         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
1671         public float[] descents = new float[INITIAL_SIZE];
1672         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
1673         public int[] flags = new int[INITIAL_SIZE]; // hasTab
1674         // breaks, widths, and flags should all have the same length
1675     }
1676 
1677     @Nullable private int[] mLeftIndents;
1678     @Nullable private int[] mRightIndents;
1679 }
1680