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