Home | History | Annotate | Download | only in text
      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.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 {
     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
     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          */
    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
    130         /* package */ void finish() {
    131             mText = null;
    132             mPaint = null;
    133             mLeftIndents = null;
    134             mRightIndents = null;
    135         }
    136 
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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)
    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      */
    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 
    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 
    594     /* package */ void generate(Builder b, boolean includepad, boolean trackpad) {
    595         final CharSequence source = b.mText;
    596         final int bufStart = b.mStart;
    597         final int bufEnd = b.mEnd;
    598         TextPaint paint = b.mPaint;
    599         int outerWidth = b.mWidth;
    600         TextDirectionHeuristic textDir = b.mTextDir;
    601         final boolean fallbackLineSpacing = b.mFallbackLineSpacing;
    602         float spacingmult = b.mSpacingMult;
    603         float spacingadd = b.mSpacingAdd;
    604         float ellipsizedWidth = b.mEllipsizedWidth;
    605         TextUtils.TruncateAt ellipsize = b.mEllipsize;
    606         final boolean addLastLineSpacing = b.mAddLastLineLineSpacing;
    607 
    608         int lineBreakCapacity = 0;
    609         int[] breaks = null;
    610         float[] lineWidths = null;
    611         float[] ascents = null;
    612         float[] descents = null;
    613         boolean[] hasTabs = null;
    614         int[] hyphenEdits = null;
    615 
    616         mLineCount = 0;
    617         mEllipsized = false;
    618         mMaxLineHeight = mMaximumVisibleLineCount < 1 ? 0 : DEFAULT_MAX_LINE_HEIGHT;
    619 
    620         int v = 0;
    621         boolean needMultiply = (spacingmult != 1 || spacingadd != 0);
    622 
    623         Paint.FontMetricsInt fm = b.mFontMetricsInt;
    624         int[] chooseHtv = null;
    625 
    626         final int[] indents;
    627         if (mLeftIndents != null || mRightIndents != null) {
    628             final int leftLen = mLeftIndents == null ? 0 : mLeftIndents.length;
    629             final int rightLen = mRightIndents == null ? 0 : mRightIndents.length;
    630             final int indentsLen = Math.max(leftLen, rightLen);
    631             indents = new int[indentsLen];
    632             for (int i = 0; i < leftLen; i++) {
    633                 indents[i] = mLeftIndents[i];
    634             }
    635             for (int i = 0; i < rightLen; i++) {
    636                 indents[i] += mRightIndents[i];
    637             }
    638         } else {
    639             indents = null;
    640         }
    641 
    642         final LineBreaker lineBreaker = new LineBreaker.Builder()
    643                 .setBreakStrategy(b.mBreakStrategy)
    644                 .setHyphenationFrequency(b.mHyphenationFrequency)
    645                 // TODO: Support more justification mode, e.g. letter spacing, stretching.
    646                 .setJustificationMode(b.mJustificationMode)
    647                 .setIndents(indents)
    648                 .build();
    649 
    650         LineBreaker.ParagraphConstraints constraints =
    651                 new LineBreaker.ParagraphConstraints();
    652 
    653         PrecomputedText.ParagraphInfo[] paragraphInfo = null;
    654         final Spanned spanned = (source instanceof Spanned) ? (Spanned) source : null;
    655         if (source instanceof PrecomputedText) {
    656             PrecomputedText precomputed = (PrecomputedText) source;
    657             final @PrecomputedText.Params.CheckResultUsableResult int checkResult =
    658                     precomputed.checkResultUsable(bufStart, bufEnd, textDir, paint,
    659                             b.mBreakStrategy, b.mHyphenationFrequency);
    660             switch (checkResult) {
    661                 case PrecomputedText.Params.UNUSABLE:
    662                     break;
    663                 case PrecomputedText.Params.NEED_RECOMPUTE:
    664                     final PrecomputedText.Params newParams =
    665                             new PrecomputedText.Params.Builder(paint)
    666                                 .setBreakStrategy(b.mBreakStrategy)
    667                                 .setHyphenationFrequency(b.mHyphenationFrequency)
    668                                 .setTextDirection(textDir)
    669                                 .build();
    670                     precomputed = PrecomputedText.create(precomputed, newParams);
    671                     paragraphInfo = precomputed.getParagraphInfo();
    672                     break;
    673                 case PrecomputedText.Params.USABLE:
    674                     // Some parameters are different from the ones when measured text is created.
    675                     paragraphInfo = precomputed.getParagraphInfo();
    676                     break;
    677             }
    678         }
    679 
    680         if (paragraphInfo == null) {
    681             final PrecomputedText.Params param = new PrecomputedText.Params(paint, textDir,
    682                     b.mBreakStrategy, b.mHyphenationFrequency);
    683             paragraphInfo = PrecomputedText.createMeasuredParagraphs(source, param, bufStart,
    684                     bufEnd, false /* computeLayout */);
    685         }
    686 
    687         for (int paraIndex = 0; paraIndex < paragraphInfo.length; paraIndex++) {
    688             final int paraStart = paraIndex == 0
    689                     ? bufStart : paragraphInfo[paraIndex - 1].paragraphEnd;
    690             final int paraEnd = paragraphInfo[paraIndex].paragraphEnd;
    691 
    692             int firstWidthLineCount = 1;
    693             int firstWidth = outerWidth;
    694             int restWidth = outerWidth;
    695 
    696             LineHeightSpan[] chooseHt = null;
    697             if (spanned != null) {
    698                 LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd,
    699                         LeadingMarginSpan.class);
    700                 for (int i = 0; i < sp.length; i++) {
    701                     LeadingMarginSpan lms = sp[i];
    702                     firstWidth -= sp[i].getLeadingMargin(true);
    703                     restWidth -= sp[i].getLeadingMargin(false);
    704 
    705                     // LeadingMarginSpan2 is odd.  The count affects all
    706                     // leading margin spans, not just this particular one
    707                     if (lms instanceof LeadingMarginSpan2) {
    708                         LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms;
    709                         firstWidthLineCount = Math.max(firstWidthLineCount,
    710                                 lms2.getLeadingMarginLineCount());
    711                     }
    712                 }
    713 
    714                 chooseHt = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class);
    715 
    716                 if (chooseHt.length == 0) {
    717                     chooseHt = null; // So that out() would not assume it has any contents
    718                 } else {
    719                     if (chooseHtv == null || chooseHtv.length < chooseHt.length) {
    720                         chooseHtv = ArrayUtils.newUnpaddedIntArray(chooseHt.length);
    721                     }
    722 
    723                     for (int i = 0; i < chooseHt.length; i++) {
    724                         int o = spanned.getSpanStart(chooseHt[i]);
    725 
    726                         if (o < paraStart) {
    727                             // starts in this layout, before the
    728                             // current paragraph
    729 
    730                             chooseHtv[i] = getLineTop(getLineForOffset(o));
    731                         } else {
    732                             // starts in this paragraph
    733 
    734                             chooseHtv[i] = v;
    735                         }
    736                     }
    737                 }
    738             }
    739             // tab stop locations
    740             float[] variableTabStops = null;
    741             if (spanned != null) {
    742                 TabStopSpan[] spans = getParagraphSpans(spanned, paraStart,
    743                         paraEnd, TabStopSpan.class);
    744                 if (spans.length > 0) {
    745                     float[] stops = new float[spans.length];
    746                     for (int i = 0; i < spans.length; i++) {
    747                         stops[i] = (float) spans[i].getTabStop();
    748                     }
    749                     Arrays.sort(stops, 0, stops.length);
    750                     variableTabStops = stops;
    751                 }
    752             }
    753 
    754             final MeasuredParagraph measuredPara = paragraphInfo[paraIndex].measured;
    755             final char[] chs = measuredPara.getChars();
    756             final int[] spanEndCache = measuredPara.getSpanEndCache().getRawArray();
    757             final int[] fmCache = measuredPara.getFontMetrics().getRawArray();
    758 
    759             constraints.setWidth(restWidth);
    760             constraints.setIndent(firstWidth, firstWidthLineCount);
    761             constraints.setTabStops(variableTabStops, TAB_INCREMENT);
    762 
    763             LineBreaker.Result res = lineBreaker.computeLineBreaks(
    764                     measuredPara.getMeasuredText(), constraints, mLineCount);
    765             int breakCount = res.getLineCount();
    766             if (lineBreakCapacity < breakCount) {
    767                 lineBreakCapacity = breakCount;
    768                 breaks = new int[lineBreakCapacity];
    769                 lineWidths = new float[lineBreakCapacity];
    770                 ascents = new float[lineBreakCapacity];
    771                 descents = new float[lineBreakCapacity];
    772                 hasTabs = new boolean[lineBreakCapacity];
    773                 hyphenEdits = new int[lineBreakCapacity];
    774             }
    775 
    776             for (int i = 0; i < breakCount; ++i) {
    777                 breaks[i] = res.getLineBreakOffset(i);
    778                 lineWidths[i] = res.getLineWidth(i);
    779                 ascents[i] = res.getLineAscent(i);
    780                 descents[i] = res.getLineDescent(i);
    781                 hasTabs[i] = res.hasLineTab(i);
    782                 hyphenEdits[i] =
    783                     packHyphenEdit(res.getStartLineHyphenEdit(i), res.getEndLineHyphenEdit(i));
    784             }
    785 
    786             final int remainingLineCount = mMaximumVisibleLineCount - mLineCount;
    787             final boolean ellipsisMayBeApplied = ellipsize != null
    788                     && (ellipsize == TextUtils.TruncateAt.END
    789                         || (mMaximumVisibleLineCount == 1
    790                                 && ellipsize != TextUtils.TruncateAt.MARQUEE));
    791             if (0 < remainingLineCount && remainingLineCount < breakCount
    792                     && ellipsisMayBeApplied) {
    793                 // Calculate width
    794                 float width = 0;
    795                 boolean hasTab = false;  // XXX May need to also have starting hyphen edit
    796                 for (int i = remainingLineCount - 1; i < breakCount; i++) {
    797                     if (i == breakCount - 1) {
    798                         width += lineWidths[i];
    799                     } else {
    800                         for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) {
    801                             width += measuredPara.getCharWidthAt(j);
    802                         }
    803                     }
    804                     hasTab |= hasTabs[i];
    805                 }
    806                 // Treat the last line and overflowed lines as a single line.
    807                 breaks[remainingLineCount - 1] = breaks[breakCount - 1];
    808                 lineWidths[remainingLineCount - 1] = width;
    809                 hasTabs[remainingLineCount - 1] = hasTab;
    810 
    811                 breakCount = remainingLineCount;
    812             }
    813 
    814             // here is the offset of the starting character of the line we are currently
    815             // measuring
    816             int here = paraStart;
    817 
    818             int fmTop = 0, fmBottom = 0, fmAscent = 0, fmDescent = 0;
    819             int fmCacheIndex = 0;
    820             int spanEndCacheIndex = 0;
    821             int breakIndex = 0;
    822             for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
    823                 // retrieve end of span
    824                 spanEnd = spanEndCache[spanEndCacheIndex++];
    825 
    826                 // retrieve cached metrics, order matches above
    827                 fm.top = fmCache[fmCacheIndex * 4 + 0];
    828                 fm.bottom = fmCache[fmCacheIndex * 4 + 1];
    829                 fm.ascent = fmCache[fmCacheIndex * 4 + 2];
    830                 fm.descent = fmCache[fmCacheIndex * 4 + 3];
    831                 fmCacheIndex++;
    832 
    833                 if (fm.top < fmTop) {
    834                     fmTop = fm.top;
    835                 }
    836                 if (fm.ascent < fmAscent) {
    837                     fmAscent = fm.ascent;
    838                 }
    839                 if (fm.descent > fmDescent) {
    840                     fmDescent = fm.descent;
    841                 }
    842                 if (fm.bottom > fmBottom) {
    843                     fmBottom = fm.bottom;
    844                 }
    845 
    846                 // skip breaks ending before current span range
    847                 while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) {
    848                     breakIndex++;
    849                 }
    850 
    851                 while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) {
    852                     int endPos = paraStart + breaks[breakIndex];
    853 
    854                     boolean moreChars = (endPos < bufEnd);
    855 
    856                     final int ascent = fallbackLineSpacing
    857                             ? Math.min(fmAscent, Math.round(ascents[breakIndex]))
    858                             : fmAscent;
    859                     final int descent = fallbackLineSpacing
    860                             ? Math.max(fmDescent, Math.round(descents[breakIndex]))
    861                             : fmDescent;
    862 
    863                     v = out(source, here, endPos,
    864                             ascent, descent, fmTop, fmBottom,
    865                             v, spacingmult, spacingadd, chooseHt, chooseHtv, fm,
    866                             hasTabs[breakIndex], hyphenEdits[breakIndex], needMultiply,
    867                             measuredPara, bufEnd, includepad, trackpad, addLastLineSpacing, chs,
    868                             paraStart, ellipsize, ellipsizedWidth, lineWidths[breakIndex],
    869                             paint, moreChars);
    870 
    871                     if (endPos < spanEnd) {
    872                         // preserve metrics for current span
    873                         fmTop = fm.top;
    874                         fmBottom = fm.bottom;
    875                         fmAscent = fm.ascent;
    876                         fmDescent = fm.descent;
    877                     } else {
    878                         fmTop = fmBottom = fmAscent = fmDescent = 0;
    879                     }
    880 
    881                     here = endPos;
    882                     breakIndex++;
    883 
    884                     if (mLineCount >= mMaximumVisibleLineCount && mEllipsized) {
    885                         return;
    886                     }
    887                 }
    888             }
    889 
    890             if (paraEnd == bufEnd) {
    891                 break;
    892             }
    893         }
    894 
    895         if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE)
    896                 && mLineCount < mMaximumVisibleLineCount) {
    897             final MeasuredParagraph measuredPara =
    898                     MeasuredParagraph.buildForBidi(source, bufEnd, bufEnd, textDir, null);
    899             paint.getFontMetricsInt(fm);
    900             v = out(source,
    901                     bufEnd, bufEnd, fm.ascent, fm.descent,
    902                     fm.top, fm.bottom,
    903                     v,
    904                     spacingmult, spacingadd, null,
    905                     null, fm, false, 0,
    906                     needMultiply, measuredPara, bufEnd,
    907                     includepad, trackpad, addLastLineSpacing, null,
    908                     bufStart, ellipsize,
    909                     ellipsizedWidth, 0, paint, false);
    910         }
    911     }
    912 
    913     private int out(final CharSequence text, final int start, final int end, int above, int below,
    914             int top, int bottom, int v, final float spacingmult, final float spacingadd,
    915             final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm,
    916             final boolean hasTab, final int hyphenEdit, final boolean needMultiply,
    917             @NonNull final MeasuredParagraph measured,
    918             final int bufEnd, final boolean includePad, final boolean trackPad,
    919             final boolean addLastLineLineSpacing, final char[] chs,
    920             final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth,
    921             final float textWidth, final TextPaint paint, final boolean moreChars) {
    922         final int j = mLineCount;
    923         final int off = j * mColumns;
    924         final int want = off + mColumns + TOP;
    925         int[] lines = mLines;
    926         final int dir = measured.getParagraphDir();
    927 
    928         if (want >= lines.length) {
    929             final int[] grow = ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(want));
    930             System.arraycopy(lines, 0, grow, 0, lines.length);
    931             mLines = grow;
    932             lines = grow;
    933         }
    934 
    935         if (j >= mLineDirections.length) {
    936             final Directions[] grow = ArrayUtils.newUnpaddedArray(Directions.class,
    937                     GrowingArrayUtils.growSize(j));
    938             System.arraycopy(mLineDirections, 0, grow, 0, mLineDirections.length);
    939             mLineDirections = grow;
    940         }
    941 
    942         if (chooseHt != null) {
    943             fm.ascent = above;
    944             fm.descent = below;
    945             fm.top = top;
    946             fm.bottom = bottom;
    947 
    948             for (int i = 0; i < chooseHt.length; i++) {
    949                 if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
    950                     ((LineHeightSpan.WithDensity) chooseHt[i])
    951                             .chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
    952                 } else {
    953                     chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
    954                 }
    955             }
    956 
    957             above = fm.ascent;
    958             below = fm.descent;
    959             top = fm.top;
    960             bottom = fm.bottom;
    961         }
    962 
    963         boolean firstLine = (j == 0);
    964         boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
    965 
    966         if (ellipsize != null) {
    967             // If there is only one line, then do any type of ellipsis except when it is MARQUEE
    968             // if there are multiple lines, just allow END ellipsis on the last line
    969             boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
    970 
    971             boolean doEllipsis =
    972                     (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) &&
    973                             ellipsize != TextUtils.TruncateAt.MARQUEE) ||
    974                     (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
    975                             ellipsize == TextUtils.TruncateAt.END);
    976             if (doEllipsis) {
    977                 calculateEllipsis(start, end, measured, widthStart,
    978                         ellipsisWidth, ellipsize, j,
    979                         textWidth, paint, forceEllipsis);
    980             }
    981         }
    982 
    983         final boolean lastLine;
    984         if (mEllipsized) {
    985             lastLine = true;
    986         } else {
    987             final boolean lastCharIsNewLine = widthStart != bufEnd && bufEnd > 0
    988                     && text.charAt(bufEnd - 1) == CHAR_NEW_LINE;
    989             if (end == bufEnd && !lastCharIsNewLine) {
    990                 lastLine = true;
    991             } else if (start == bufEnd && lastCharIsNewLine) {
    992                 lastLine = true;
    993             } else {
    994                 lastLine = false;
    995             }
    996         }
    997 
    998         if (firstLine) {
    999             if (trackPad) {
   1000                 mTopPadding = top - above;
   1001             }
   1002 
   1003             if (includePad) {
   1004                 above = top;
   1005             }
   1006         }
   1007 
   1008         int extra;
   1009 
   1010         if (lastLine) {
   1011             if (trackPad) {
   1012                 mBottomPadding = bottom - below;
   1013             }
   1014 
   1015             if (includePad) {
   1016                 below = bottom;
   1017             }
   1018         }
   1019 
   1020         if (needMultiply && (addLastLineLineSpacing || !lastLine)) {
   1021             double ex = (below - above) * (spacingmult - 1) + spacingadd;
   1022             if (ex >= 0) {
   1023                 extra = (int)(ex + EXTRA_ROUNDING);
   1024             } else {
   1025                 extra = -(int)(-ex + EXTRA_ROUNDING);
   1026             }
   1027         } else {
   1028             extra = 0;
   1029         }
   1030 
   1031         lines[off + START] = start;
   1032         lines[off + TOP] = v;
   1033         lines[off + DESCENT] = below + extra;
   1034         lines[off + EXTRA] = extra;
   1035 
   1036         // special case for non-ellipsized last visible line when maxLines is set
   1037         // store the height as if it was ellipsized
   1038         if (!mEllipsized && currentLineIsTheLastVisibleOne) {
   1039             // below calculation as if it was the last line
   1040             int maxLineBelow = includePad ? bottom : below;
   1041             // similar to the calculation of v below, without the extra.
   1042             mMaxLineHeight = v + (maxLineBelow - above);
   1043         }
   1044 
   1045         v += (below - above) + extra;
   1046         lines[off + mColumns + START] = end;
   1047         lines[off + mColumns + TOP] = v;
   1048 
   1049         // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining
   1050         // one bit for start field
   1051         lines[off + TAB] |= hasTab ? TAB_MASK : 0;
   1052         lines[off + HYPHEN] = hyphenEdit;
   1053         lines[off + DIR] |= dir << DIR_SHIFT;
   1054         mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart);
   1055 
   1056         mLineCount++;
   1057         return v;
   1058     }
   1059 
   1060     private void calculateEllipsis(int lineStart, int lineEnd,
   1061                                    MeasuredParagraph measured, int widthStart,
   1062                                    float avail, TextUtils.TruncateAt where,
   1063                                    int line, float textWidth, TextPaint paint,
   1064                                    boolean forceEllipsis) {
   1065         avail -= getTotalInsets(line);
   1066         if (textWidth <= avail && !forceEllipsis) {
   1067             // Everything fits!
   1068             mLines[mColumns * line + ELLIPSIS_START] = 0;
   1069             mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
   1070             return;
   1071         }
   1072 
   1073         float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where));
   1074         int ellipsisStart = 0;
   1075         int ellipsisCount = 0;
   1076         int len = lineEnd - lineStart;
   1077 
   1078         // We only support start ellipsis on a single line
   1079         if (where == TextUtils.TruncateAt.START) {
   1080             if (mMaximumVisibleLineCount == 1) {
   1081                 float sum = 0;
   1082                 int i;
   1083 
   1084                 for (i = len; i > 0; i--) {
   1085                     float w = measured.getCharWidthAt(i - 1 + lineStart - widthStart);
   1086                     if (w + sum + ellipsisWidth > avail) {
   1087                         while (i < len
   1088                                 && measured.getCharWidthAt(i + lineStart - widthStart) == 0.0f) {
   1089                             i++;
   1090                         }
   1091                         break;
   1092                     }
   1093 
   1094                     sum += w;
   1095                 }
   1096 
   1097                 ellipsisStart = 0;
   1098                 ellipsisCount = i;
   1099             } else {
   1100                 if (Log.isLoggable(TAG, Log.WARN)) {
   1101                     Log.w(TAG, "Start Ellipsis only supported with one line");
   1102                 }
   1103             }
   1104         } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
   1105                 where == TextUtils.TruncateAt.END_SMALL) {
   1106             float sum = 0;
   1107             int i;
   1108 
   1109             for (i = 0; i < len; i++) {
   1110                 float w = measured.getCharWidthAt(i + lineStart - widthStart);
   1111 
   1112                 if (w + sum + ellipsisWidth > avail) {
   1113                     break;
   1114                 }
   1115 
   1116                 sum += w;
   1117             }
   1118 
   1119             ellipsisStart = i;
   1120             ellipsisCount = len - i;
   1121             if (forceEllipsis && ellipsisCount == 0 && len > 0) {
   1122                 ellipsisStart = len - 1;
   1123                 ellipsisCount = 1;
   1124             }
   1125         } else {
   1126             // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line
   1127             if (mMaximumVisibleLineCount == 1) {
   1128                 float lsum = 0, rsum = 0;
   1129                 int left = 0, right = len;
   1130 
   1131                 float ravail = (avail - ellipsisWidth) / 2;
   1132                 for (right = len; right > 0; right--) {
   1133                     float w = measured.getCharWidthAt(right - 1 + lineStart - widthStart);
   1134 
   1135                     if (w + rsum > ravail) {
   1136                         while (right < len
   1137                                 && measured.getCharWidthAt(right + lineStart - widthStart)
   1138                                     == 0.0f) {
   1139                             right++;
   1140                         }
   1141                         break;
   1142                     }
   1143                     rsum += w;
   1144                 }
   1145 
   1146                 float lavail = avail - ellipsisWidth - rsum;
   1147                 for (left = 0; left < right; left++) {
   1148                     float w = measured.getCharWidthAt(left + lineStart - widthStart);
   1149 
   1150                     if (w + lsum > lavail) {
   1151                         break;
   1152                     }
   1153 
   1154                     lsum += w;
   1155                 }
   1156 
   1157                 ellipsisStart = left;
   1158                 ellipsisCount = right - left;
   1159             } else {
   1160                 if (Log.isLoggable(TAG, Log.WARN)) {
   1161                     Log.w(TAG, "Middle Ellipsis only supported with one line");
   1162                 }
   1163             }
   1164         }
   1165         mEllipsized = true;
   1166         mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
   1167         mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
   1168     }
   1169 
   1170     private float getTotalInsets(int line) {
   1171         int totalIndent = 0;
   1172         if (mLeftIndents != null) {
   1173             totalIndent = mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
   1174         }
   1175         if (mRightIndents != null) {
   1176             totalIndent += mRightIndents[Math.min(line, mRightIndents.length - 1)];
   1177         }
   1178         return totalIndent;
   1179     }
   1180 
   1181     // Override the base class so we can directly access our members,
   1182     // rather than relying on member functions.
   1183     // The logic mirrors that of Layout.getLineForVertical
   1184     // FIXME: It may be faster to do a linear search for layouts without many lines.
   1185     @Override
   1186     public int getLineForVertical(int vertical) {
   1187         int high = mLineCount;
   1188         int low = -1;
   1189         int guess;
   1190         int[] lines = mLines;
   1191         while (high - low > 1) {
   1192             guess = (high + low) >> 1;
   1193             if (lines[mColumns * guess + TOP] > vertical){
   1194                 high = guess;
   1195             } else {
   1196                 low = guess;
   1197             }
   1198         }
   1199         if (low < 0) {
   1200             return 0;
   1201         } else {
   1202             return low;
   1203         }
   1204     }
   1205 
   1206     @Override
   1207     public int getLineCount() {
   1208         return mLineCount;
   1209     }
   1210 
   1211     @Override
   1212     public int getLineTop(int line) {
   1213         return mLines[mColumns * line + TOP];
   1214     }
   1215 
   1216     /**
   1217      * @hide
   1218      */
   1219     @Override
   1220     public int getLineExtra(int line) {
   1221         return mLines[mColumns * line + EXTRA];
   1222     }
   1223 
   1224     @Override
   1225     public int getLineDescent(int line) {
   1226         return mLines[mColumns * line + DESCENT];
   1227     }
   1228 
   1229     @Override
   1230     public int getLineStart(int line) {
   1231         return mLines[mColumns * line + START] & START_MASK;
   1232     }
   1233 
   1234     @Override
   1235     public int getParagraphDirection(int line) {
   1236         return mLines[mColumns * line + DIR] >> DIR_SHIFT;
   1237     }
   1238 
   1239     @Override
   1240     public boolean getLineContainsTab(int line) {
   1241         return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
   1242     }
   1243 
   1244     @Override
   1245     public final Directions getLineDirections(int line) {
   1246         if (line > getLineCount()) {
   1247             throw new ArrayIndexOutOfBoundsException();
   1248         }
   1249         return mLineDirections[line];
   1250     }
   1251 
   1252     @Override
   1253     public int getTopPadding() {
   1254         return mTopPadding;
   1255     }
   1256 
   1257     @Override
   1258     public int getBottomPadding() {
   1259         return mBottomPadding;
   1260     }
   1261 
   1262     // To store into single int field, pack the pair of start and end hyphen edit.
   1263     static int packHyphenEdit(
   1264             @Paint.StartHyphenEdit int start, @Paint.EndHyphenEdit int end) {
   1265         return start << START_HYPHEN_BITS_SHIFT | end;
   1266     }
   1267 
   1268     static int unpackStartHyphenEdit(int packedHyphenEdit) {
   1269         return (packedHyphenEdit & START_HYPHEN_MASK) >> START_HYPHEN_BITS_SHIFT;
   1270     }
   1271 
   1272     static int unpackEndHyphenEdit(int packedHyphenEdit) {
   1273         return packedHyphenEdit & END_HYPHEN_MASK;
   1274     }
   1275 
   1276     /**
   1277      * Returns the start hyphen edit value for this line.
   1278      *
   1279      * @param lineNumber a line number
   1280      * @return A start hyphen edit value.
   1281      * @hide
   1282      */
   1283     @Override
   1284     public @Paint.StartHyphenEdit int getStartHyphenEdit(int lineNumber) {
   1285         return unpackStartHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK);
   1286     }
   1287 
   1288     /**
   1289      * Returns the packed hyphen edit value for this line.
   1290      *
   1291      * @param lineNumber a line number
   1292      * @return An end hyphen edit value.
   1293      * @hide
   1294      */
   1295     @Override
   1296     public @Paint.EndHyphenEdit int getEndHyphenEdit(int lineNumber) {
   1297         return unpackEndHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK);
   1298     }
   1299 
   1300     /**
   1301      * @hide
   1302      */
   1303     @Override
   1304     public int getIndentAdjust(int line, Alignment align) {
   1305         if (align == Alignment.ALIGN_LEFT) {
   1306             if (mLeftIndents == null) {
   1307                 return 0;
   1308             } else {
   1309                 return mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
   1310             }
   1311         } else if (align == Alignment.ALIGN_RIGHT) {
   1312             if (mRightIndents == null) {
   1313                 return 0;
   1314             } else {
   1315                 return -mRightIndents[Math.min(line, mRightIndents.length - 1)];
   1316             }
   1317         } else if (align == Alignment.ALIGN_CENTER) {
   1318             int left = 0;
   1319             if (mLeftIndents != null) {
   1320                 left = mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
   1321             }
   1322             int right = 0;
   1323             if (mRightIndents != null) {
   1324                 right = mRightIndents[Math.min(line, mRightIndents.length - 1)];
   1325             }
   1326             return (left - right) >> 1;
   1327         } else {
   1328             throw new AssertionError("unhandled alignment " + align);
   1329         }
   1330     }
   1331 
   1332     @Override
   1333     public int getEllipsisCount(int line) {
   1334         if (mColumns < COLUMNS_ELLIPSIZE) {
   1335             return 0;
   1336         }
   1337 
   1338         return mLines[mColumns * line + ELLIPSIS_COUNT];
   1339     }
   1340 
   1341     @Override
   1342     public int getEllipsisStart(int line) {
   1343         if (mColumns < COLUMNS_ELLIPSIZE) {
   1344             return 0;
   1345         }
   1346 
   1347         return mLines[mColumns * line + ELLIPSIS_START];
   1348     }
   1349 
   1350     @Override
   1351     public int getEllipsizedWidth() {
   1352         return mEllipsizedWidth;
   1353     }
   1354 
   1355     /**
   1356      * Return the total height of this layout.
   1357      *
   1358      * @param cap if true and max lines is set, returns the height of the layout at the max lines.
   1359      *
   1360      * @hide
   1361      */
   1362     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
   1363     public int getHeight(boolean cap) {
   1364         if (cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight == -1
   1365                 && Log.isLoggable(TAG, Log.WARN)) {
   1366             Log.w(TAG, "maxLineHeight should not be -1. "
   1367                     + " maxLines:" + mMaximumVisibleLineCount
   1368                     + " lineCount:" + mLineCount);
   1369         }
   1370 
   1371         return cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight != -1
   1372                 ? mMaxLineHeight : super.getHeight();
   1373     }
   1374 
   1375     @UnsupportedAppUsage
   1376     private int mLineCount;
   1377     private int mTopPadding, mBottomPadding;
   1378     @UnsupportedAppUsage
   1379     private int mColumns;
   1380     private int mEllipsizedWidth;
   1381 
   1382     /**
   1383      * Keeps track if ellipsize is applied to the text.
   1384      */
   1385     private boolean mEllipsized;
   1386 
   1387     /**
   1388      * If maxLines is set, ellipsize is not set, and the actual line count of text is greater than
   1389      * or equal to maxLine, this variable holds the ideal visual height of the maxLine'th line
   1390      * starting from the top of the layout. If maxLines is not set its value will be -1.
   1391      *
   1392      * The value is the same as getLineTop(maxLines) for ellipsized version where structurally no
   1393      * more than maxLines is contained.
   1394      */
   1395     private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT;
   1396 
   1397     private static final int COLUMNS_NORMAL = 5;
   1398     private static final int COLUMNS_ELLIPSIZE = 7;
   1399     private static final int START = 0;
   1400     private static final int DIR = START;
   1401     private static final int TAB = START;
   1402     private static final int TOP = 1;
   1403     private static final int DESCENT = 2;
   1404     private static final int EXTRA = 3;
   1405     private static final int HYPHEN = 4;
   1406     @UnsupportedAppUsage
   1407     private static final int ELLIPSIS_START = 5;
   1408     private static final int ELLIPSIS_COUNT = 6;
   1409 
   1410     @UnsupportedAppUsage
   1411     private int[] mLines;
   1412     @UnsupportedAppUsage
   1413     private Directions[] mLineDirections;
   1414     @UnsupportedAppUsage
   1415     private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
   1416 
   1417     private static final int START_MASK = 0x1FFFFFFF;
   1418     private static final int DIR_SHIFT  = 30;
   1419     private static final int TAB_MASK   = 0x20000000;
   1420     private static final int HYPHEN_MASK = 0xFF;
   1421     private static final int START_HYPHEN_BITS_SHIFT = 3;
   1422     private static final int START_HYPHEN_MASK = 0x18; // 0b11000
   1423     private static final int END_HYPHEN_MASK = 0x7;  // 0b00111
   1424 
   1425     private static final float TAB_INCREMENT = 20; // same as Layout, but that's private
   1426 
   1427     private static final char CHAR_NEW_LINE = '\n';
   1428 
   1429     private static final double EXTRA_ROUNDING = 0.5;
   1430 
   1431     private static final int DEFAULT_MAX_LINE_HEIGHT = -1;
   1432 
   1433     // Unused, here because of gray list private API accesses.
   1434     /*package*/ static class LineBreaks {
   1435         private static final int INITIAL_SIZE = 16;
   1436         @UnsupportedAppUsage
   1437         public int[] breaks = new int[INITIAL_SIZE];
   1438         @UnsupportedAppUsage
   1439         public float[] widths = new float[INITIAL_SIZE];
   1440         @UnsupportedAppUsage
   1441         public float[] ascents = new float[INITIAL_SIZE];
   1442         @UnsupportedAppUsage
   1443         public float[] descents = new float[INITIAL_SIZE];
   1444         @UnsupportedAppUsage
   1445         public int[] flags = new int[INITIAL_SIZE]; // hasTab
   1446         // breaks, widths, and flags should all have the same length
   1447     }
   1448 
   1449     @Nullable private int[] mLeftIndents;
   1450     @Nullable private int[] mRightIndents;
   1451 }
   1452