• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.text;
18 
19 import 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             } else {
981                 mLines[mColumns * j + ELLIPSIS_START] = 0;
982                 mLines[mColumns * j + ELLIPSIS_COUNT] = 0;
983             }
984         }
985 
986         final boolean lastLine;
987         if (mEllipsized) {
988             lastLine = true;
989         } else {
990             final boolean lastCharIsNewLine = widthStart != bufEnd && bufEnd > 0
991                     && text.charAt(bufEnd - 1) == CHAR_NEW_LINE;
992             if (end == bufEnd && !lastCharIsNewLine) {
993                 lastLine = true;
994             } else if (start == bufEnd && lastCharIsNewLine) {
995                 lastLine = true;
996             } else {
997                 lastLine = false;
998             }
999         }
1000 
1001         if (firstLine) {
1002             if (trackPad) {
1003                 mTopPadding = top - above;
1004             }
1005 
1006             if (includePad) {
1007                 above = top;
1008             }
1009         }
1010 
1011         int extra;
1012 
1013         if (lastLine) {
1014             if (trackPad) {
1015                 mBottomPadding = bottom - below;
1016             }
1017 
1018             if (includePad) {
1019                 below = bottom;
1020             }
1021         }
1022 
1023         if (needMultiply && (addLastLineLineSpacing || !lastLine)) {
1024             double ex = (below - above) * (spacingmult - 1) + spacingadd;
1025             if (ex >= 0) {
1026                 extra = (int)(ex + EXTRA_ROUNDING);
1027             } else {
1028                 extra = -(int)(-ex + EXTRA_ROUNDING);
1029             }
1030         } else {
1031             extra = 0;
1032         }
1033 
1034         lines[off + START] = start;
1035         lines[off + TOP] = v;
1036         lines[off + DESCENT] = below + extra;
1037         lines[off + EXTRA] = extra;
1038 
1039         // special case for non-ellipsized last visible line when maxLines is set
1040         // store the height as if it was ellipsized
1041         if (!mEllipsized && currentLineIsTheLastVisibleOne) {
1042             // below calculation as if it was the last line
1043             int maxLineBelow = includePad ? bottom : below;
1044             // similar to the calculation of v below, without the extra.
1045             mMaxLineHeight = v + (maxLineBelow - above);
1046         }
1047 
1048         v += (below - above) + extra;
1049         lines[off + mColumns + START] = end;
1050         lines[off + mColumns + TOP] = v;
1051 
1052         // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining
1053         // one bit for start field
1054         lines[off + TAB] |= hasTab ? TAB_MASK : 0;
1055         lines[off + HYPHEN] = hyphenEdit;
1056         lines[off + DIR] |= dir << DIR_SHIFT;
1057         mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart);
1058 
1059         mLineCount++;
1060         return v;
1061     }
1062 
1063     private void calculateEllipsis(int lineStart, int lineEnd,
1064                                    MeasuredParagraph measured, int widthStart,
1065                                    float avail, TextUtils.TruncateAt where,
1066                                    int line, float textWidth, TextPaint paint,
1067                                    boolean forceEllipsis) {
1068         avail -= getTotalInsets(line);
1069         if (textWidth <= avail && !forceEllipsis) {
1070             // Everything fits!
1071             mLines[mColumns * line + ELLIPSIS_START] = 0;
1072             mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
1073             return;
1074         }
1075 
1076         float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where));
1077         int ellipsisStart = 0;
1078         int ellipsisCount = 0;
1079         int len = lineEnd - lineStart;
1080 
1081         // We only support start ellipsis on a single line
1082         if (where == TextUtils.TruncateAt.START) {
1083             if (mMaximumVisibleLineCount == 1) {
1084                 float sum = 0;
1085                 int i;
1086 
1087                 for (i = len; i > 0; i--) {
1088                     float w = measured.getCharWidthAt(i - 1 + lineStart - widthStart);
1089                     if (w + sum + ellipsisWidth > avail) {
1090                         while (i < len
1091                                 && measured.getCharWidthAt(i + lineStart - widthStart) == 0.0f) {
1092                             i++;
1093                         }
1094                         break;
1095                     }
1096 
1097                     sum += w;
1098                 }
1099 
1100                 ellipsisStart = 0;
1101                 ellipsisCount = i;
1102             } else {
1103                 if (Log.isLoggable(TAG, Log.WARN)) {
1104                     Log.w(TAG, "Start Ellipsis only supported with one line");
1105                 }
1106             }
1107         } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
1108                 where == TextUtils.TruncateAt.END_SMALL) {
1109             float sum = 0;
1110             int i;
1111 
1112             for (i = 0; i < len; i++) {
1113                 float w = measured.getCharWidthAt(i + lineStart - widthStart);
1114 
1115                 if (w + sum + ellipsisWidth > avail) {
1116                     break;
1117                 }
1118 
1119                 sum += w;
1120             }
1121 
1122             ellipsisStart = i;
1123             ellipsisCount = len - i;
1124             if (forceEllipsis && ellipsisCount == 0 && len > 0) {
1125                 ellipsisStart = len - 1;
1126                 ellipsisCount = 1;
1127             }
1128         } else {
1129             // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line
1130             if (mMaximumVisibleLineCount == 1) {
1131                 float lsum = 0, rsum = 0;
1132                 int left = 0, right = len;
1133 
1134                 float ravail = (avail - ellipsisWidth) / 2;
1135                 for (right = len; right > 0; right--) {
1136                     float w = measured.getCharWidthAt(right - 1 + lineStart - widthStart);
1137 
1138                     if (w + rsum > ravail) {
1139                         while (right < len
1140                                 && measured.getCharWidthAt(right + lineStart - widthStart)
1141                                     == 0.0f) {
1142                             right++;
1143                         }
1144                         break;
1145                     }
1146                     rsum += w;
1147                 }
1148 
1149                 float lavail = avail - ellipsisWidth - rsum;
1150                 for (left = 0; left < right; left++) {
1151                     float w = measured.getCharWidthAt(left + lineStart - widthStart);
1152 
1153                     if (w + lsum > lavail) {
1154                         break;
1155                     }
1156 
1157                     lsum += w;
1158                 }
1159 
1160                 ellipsisStart = left;
1161                 ellipsisCount = right - left;
1162             } else {
1163                 if (Log.isLoggable(TAG, Log.WARN)) {
1164                     Log.w(TAG, "Middle Ellipsis only supported with one line");
1165                 }
1166             }
1167         }
1168         mEllipsized = true;
1169         mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
1170         mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
1171     }
1172 
1173     private float getTotalInsets(int line) {
1174         int totalIndent = 0;
1175         if (mLeftIndents != null) {
1176             totalIndent = mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
1177         }
1178         if (mRightIndents != null) {
1179             totalIndent += mRightIndents[Math.min(line, mRightIndents.length - 1)];
1180         }
1181         return totalIndent;
1182     }
1183 
1184     // Override the base class so we can directly access our members,
1185     // rather than relying on member functions.
1186     // The logic mirrors that of Layout.getLineForVertical
1187     // FIXME: It may be faster to do a linear search for layouts without many lines.
1188     @Override
1189     public int getLineForVertical(int vertical) {
1190         int high = mLineCount;
1191         int low = -1;
1192         int guess;
1193         int[] lines = mLines;
1194         while (high - low > 1) {
1195             guess = (high + low) >> 1;
1196             if (lines[mColumns * guess + TOP] > vertical){
1197                 high = guess;
1198             } else {
1199                 low = guess;
1200             }
1201         }
1202         if (low < 0) {
1203             return 0;
1204         } else {
1205             return low;
1206         }
1207     }
1208 
1209     @Override
1210     public int getLineCount() {
1211         return mLineCount;
1212     }
1213 
1214     @Override
1215     public int getLineTop(int line) {
1216         return mLines[mColumns * line + TOP];
1217     }
1218 
1219     /**
1220      * @hide
1221      */
1222     @Override
1223     public int getLineExtra(int line) {
1224         return mLines[mColumns * line + EXTRA];
1225     }
1226 
1227     @Override
1228     public int getLineDescent(int line) {
1229         return mLines[mColumns * line + DESCENT];
1230     }
1231 
1232     @Override
1233     public int getLineStart(int line) {
1234         return mLines[mColumns * line + START] & START_MASK;
1235     }
1236 
1237     @Override
1238     public int getParagraphDirection(int line) {
1239         return mLines[mColumns * line + DIR] >> DIR_SHIFT;
1240     }
1241 
1242     @Override
1243     public boolean getLineContainsTab(int line) {
1244         return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
1245     }
1246 
1247     @Override
1248     public final Directions getLineDirections(int line) {
1249         if (line > getLineCount()) {
1250             throw new ArrayIndexOutOfBoundsException();
1251         }
1252         return mLineDirections[line];
1253     }
1254 
1255     @Override
1256     public int getTopPadding() {
1257         return mTopPadding;
1258     }
1259 
1260     @Override
1261     public int getBottomPadding() {
1262         return mBottomPadding;
1263     }
1264 
1265     // To store into single int field, pack the pair of start and end hyphen edit.
1266     static int packHyphenEdit(
1267             @Paint.StartHyphenEdit int start, @Paint.EndHyphenEdit int end) {
1268         return start << START_HYPHEN_BITS_SHIFT | end;
1269     }
1270 
1271     static int unpackStartHyphenEdit(int packedHyphenEdit) {
1272         return (packedHyphenEdit & START_HYPHEN_MASK) >> START_HYPHEN_BITS_SHIFT;
1273     }
1274 
1275     static int unpackEndHyphenEdit(int packedHyphenEdit) {
1276         return packedHyphenEdit & END_HYPHEN_MASK;
1277     }
1278 
1279     /**
1280      * Returns the start hyphen edit value for this line.
1281      *
1282      * @param lineNumber a line number
1283      * @return A start hyphen edit value.
1284      * @hide
1285      */
1286     @Override
1287     public @Paint.StartHyphenEdit int getStartHyphenEdit(int lineNumber) {
1288         return unpackStartHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK);
1289     }
1290 
1291     /**
1292      * Returns the packed hyphen edit value for this line.
1293      *
1294      * @param lineNumber a line number
1295      * @return An end hyphen edit value.
1296      * @hide
1297      */
1298     @Override
1299     public @Paint.EndHyphenEdit int getEndHyphenEdit(int lineNumber) {
1300         return unpackEndHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK);
1301     }
1302 
1303     /**
1304      * @hide
1305      */
1306     @Override
1307     public int getIndentAdjust(int line, Alignment align) {
1308         if (align == Alignment.ALIGN_LEFT) {
1309             if (mLeftIndents == null) {
1310                 return 0;
1311             } else {
1312                 return mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
1313             }
1314         } else if (align == Alignment.ALIGN_RIGHT) {
1315             if (mRightIndents == null) {
1316                 return 0;
1317             } else {
1318                 return -mRightIndents[Math.min(line, mRightIndents.length - 1)];
1319             }
1320         } else if (align == Alignment.ALIGN_CENTER) {
1321             int left = 0;
1322             if (mLeftIndents != null) {
1323                 left = mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
1324             }
1325             int right = 0;
1326             if (mRightIndents != null) {
1327                 right = mRightIndents[Math.min(line, mRightIndents.length - 1)];
1328             }
1329             return (left - right) >> 1;
1330         } else {
1331             throw new AssertionError("unhandled alignment " + align);
1332         }
1333     }
1334 
1335     @Override
1336     public int getEllipsisCount(int line) {
1337         if (mColumns < COLUMNS_ELLIPSIZE) {
1338             return 0;
1339         }
1340 
1341         return mLines[mColumns * line + ELLIPSIS_COUNT];
1342     }
1343 
1344     @Override
1345     public int getEllipsisStart(int line) {
1346         if (mColumns < COLUMNS_ELLIPSIZE) {
1347             return 0;
1348         }
1349 
1350         return mLines[mColumns * line + ELLIPSIS_START];
1351     }
1352 
1353     @Override
1354     public int getEllipsizedWidth() {
1355         return mEllipsizedWidth;
1356     }
1357 
1358     /**
1359      * Return the total height of this layout.
1360      *
1361      * @param cap if true and max lines is set, returns the height of the layout at the max lines.
1362      *
1363      * @hide
1364      */
1365     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
1366     public int getHeight(boolean cap) {
1367         if (cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight == -1
1368                 && Log.isLoggable(TAG, Log.WARN)) {
1369             Log.w(TAG, "maxLineHeight should not be -1. "
1370                     + " maxLines:" + mMaximumVisibleLineCount
1371                     + " lineCount:" + mLineCount);
1372         }
1373 
1374         return cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight != -1
1375                 ? mMaxLineHeight : super.getHeight();
1376     }
1377 
1378     @UnsupportedAppUsage
1379     private int mLineCount;
1380     private int mTopPadding, mBottomPadding;
1381     @UnsupportedAppUsage
1382     private int mColumns;
1383     private int mEllipsizedWidth;
1384 
1385     /**
1386      * Keeps track if ellipsize is applied to the text.
1387      */
1388     private boolean mEllipsized;
1389 
1390     /**
1391      * If maxLines is set, ellipsize is not set, and the actual line count of text is greater than
1392      * or equal to maxLine, this variable holds the ideal visual height of the maxLine'th line
1393      * starting from the top of the layout. If maxLines is not set its value will be -1.
1394      *
1395      * The value is the same as getLineTop(maxLines) for ellipsized version where structurally no
1396      * more than maxLines is contained.
1397      */
1398     private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT;
1399 
1400     private static final int COLUMNS_NORMAL = 5;
1401     private static final int COLUMNS_ELLIPSIZE = 7;
1402     private static final int START = 0;
1403     private static final int DIR = START;
1404     private static final int TAB = START;
1405     private static final int TOP = 1;
1406     private static final int DESCENT = 2;
1407     private static final int EXTRA = 3;
1408     private static final int HYPHEN = 4;
1409     @UnsupportedAppUsage
1410     private static final int ELLIPSIS_START = 5;
1411     private static final int ELLIPSIS_COUNT = 6;
1412 
1413     @UnsupportedAppUsage
1414     private int[] mLines;
1415     @UnsupportedAppUsage
1416     private Directions[] mLineDirections;
1417     @UnsupportedAppUsage
1418     private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
1419 
1420     private static final int START_MASK = 0x1FFFFFFF;
1421     private static final int DIR_SHIFT  = 30;
1422     private static final int TAB_MASK   = 0x20000000;
1423     private static final int HYPHEN_MASK = 0xFF;
1424     private static final int START_HYPHEN_BITS_SHIFT = 3;
1425     private static final int START_HYPHEN_MASK = 0x18; // 0b11000
1426     private static final int END_HYPHEN_MASK = 0x7;  // 0b00111
1427 
1428     private static final float TAB_INCREMENT = 20; // same as Layout, but that's private
1429 
1430     private static final char CHAR_NEW_LINE = '\n';
1431 
1432     private static final double EXTRA_ROUNDING = 0.5;
1433 
1434     private static final int DEFAULT_MAX_LINE_HEIGHT = -1;
1435 
1436     // Unused, here because of gray list private API accesses.
1437     /*package*/ static class LineBreaks {
1438         private static final int INITIAL_SIZE = 16;
1439         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
1440         public int[] breaks = new int[INITIAL_SIZE];
1441         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
1442         public float[] widths = new float[INITIAL_SIZE];
1443         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
1444         public float[] ascents = new float[INITIAL_SIZE];
1445         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
1446         public float[] descents = new float[INITIAL_SIZE];
1447         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
1448         public int[] flags = new int[INITIAL_SIZE]; // hasTab
1449         // breaks, widths, and flags should all have the same length
1450     }
1451 
1452     @Nullable private int[] mLeftIndents;
1453     @Nullable private int[] mRightIndents;
1454 }
1455