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