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