content: /*
   * Copyright (C) 2006 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.text;
  
  import android.annotation.FloatRange;
  import android.annotation.IntRange;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.UnsupportedAppUsage;
  import android.graphics.Paint;
  import android.graphics.text.LineBreaker;
  import android.os.Build;
  import android.text.style.LeadingMarginSpan;
  import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
  import android.text.style.LineHeightSpan;
  import android.text.style.TabStopSpan;
  import android.util.Log;
  import android.util.Pools.SynchronizedPool;
  
  import com.android.internal.util.ArrayUtils;
  import com.android.internal.util.GrowingArrayUtils;
  
  import java.util.Arrays;
  
  /**
   * StaticLayout is a Layout for text that will not be edited after it
   * is laid out.  Use {@link DynamicLayout} for text that may change.
   * <p>This is used by widgets to control text layout. You should not need
   * to use this class directly unless you are implementing your own widget
   * or custom display object, or would be tempted to call
   * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int,
   * float, float, android.graphics.Paint)
   * Canvas.drawText()} directly.</p>
   */
  public class StaticLayout extends Layout {
      /*
       * The break iteration is done in native code. The protocol for using the native code is as
       * follows.
       *
       * First, call nInit to setup native line breaker object. Then, for each paragraph, do the
       * following:
       *
       *   - Create MeasuredParagraph by MeasuredParagraph.buildForStaticLayout which measures in
       *     native.
       *   - Run LineBreaker.computeLineBreaks() to obtain line breaks for the paragraph.
       *
       * After all paragraphs, call finish() to release expensive buffers.
       */
  
      static final String TAG = "StaticLayout";
  
      /**
       * Builder for static layouts. The builder is the preferred pattern for constructing
       * StaticLayout objects and should be preferred over the constructors, particularly to access
       * newer features. To build a static layout, first call {@link #obtain} with the required
       * arguments (text, paint, and width), then call setters for optional parameters, and finally
       * {@link #build} to build the StaticLayout object. Parameters not explicitly set will get
       * default values.
       */
      public final static class Builder {
          private Builder() {}
  
          /**
           * Obtain a builder for constructing StaticLayout objects.
           *
           * @param source The text to be laid out, optionally with spans
           * @param start The index of the start of the text
           * @param end The index + 1 of the end of the text
           * @param paint The base paint used for layout
           * @param width The width in pixels
           * @return a builder object used for constructing the StaticLayout
           */
          @NonNull
          public static Builder obtain(@NonNull CharSequence source, @IntRange(from = 0) int start,
                  @IntRange(from = 0) int end, @NonNull TextPaint paint,
                  @IntRange(from = 0) int width) {
              Builder b = sPool.acquire();
              if (b == null) {
                  b = new Builder();
              }
  
              // set default initial values
              b.mText = source;
              b.mStart = start;
              b.mEnd = end;
              b.mPaint = paint;
              b.mWidth = width;
              b.mAlignment = Alignment.ALIGN_NORMAL;
              b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
              b.mSpacingMult = DEFAULT_LINESPACING_MULTIPLIER;
              b.mSpacingAdd = DEFAULT_LINESPACING_ADDITION;
              b.mIncludePad = true;
              b.mFallbackLineSpacing = false;
              b.mEllipsizedWidth = width;
              b.mEllipsize = null;
              b.mMaxLines = Integer.MAX_VALUE;
              b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
              b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
              b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
              return b;
          }
  
          /**
           * This method should be called after the layout is finished getting constructed and the
           * builder needs to be cleaned up and returned to the pool.
           */
          private static void recycle(@NonNull Builder b) {
              b.mPaint = null;
              b.mText = null;
              b.mLeftIndents = null;
              b.mRightIndents = null;
              sPool.release(b);
          }
  
          // release any expensive state
          /* package */ void finish() {
              mText = null;
              mPaint = null;
              mLeftIndents = null;
              mRightIndents = null;
          }
  
          public Builder setText(CharSequence source) {
              return setText(source, 0, source.length());
          }
  
          /**
           * Set the text. Only useful when re-using the builder, which is done for
           * the internal implementation of {@link DynamicLayout} but not as part
           * of normal {@link StaticLayout} usage.
           *
           * @param source The text to be laid out, optionally with spans
           * @param start The index of the start of the text
           * @param end The index + 1 of the end of the text
           * @return this builder, useful for chaining
           *
           * @hide
           */
          @NonNull
          public Builder setText(@NonNull CharSequence source, int start, int end) {
              mText = source;
              mStart = start;
              mEnd = end;
              return this;
          }
  
          /**
           * Set the paint. Internal for reuse cases only.
           *
           * @param paint The base paint used for layout
           * @return this builder, useful for chaining
           *
           * @hide
           */
          @NonNull
          public Builder setPaint(@NonNull TextPaint paint) {
              mPaint = paint;
              return this;
          }
  
          /**
           * Set the width. Internal for reuse cases only.
           *
           * @param width The width in pixels
           * @return this builder, useful for chaining
           *
           * @hide
           */
          @NonNull
          public Builder setWidth(@IntRange(from = 0) int width) {
              mWidth = width;
              if (mEllipsize == null) {
                  mEllipsizedWidth = width;
              }
              return this;
          }
  
          /**
           * Set the alignment. The default is {@link Layout.Alignment#ALIGN_NORMAL}.
           *
           * @param alignment Alignment for the resulting {@link StaticLayout}
           * @return this builder, useful for chaining
           */
          @NonNull
          public Builder setAlignment(@NonNull Alignment alignment) {
              mAlignment = alignment;
              return this;
          }
  
          /**
           * Set the text direction heuristic. The text direction heuristic is used to
           * resolve text direction per-paragraph based on the input text. The default is
           * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}.
           *
           * @param textDir text direction heuristic for resolving bidi behavior.
           * @return this builder, useful for chaining
           */
          @NonNull
          public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) {
              mTextDir = textDir;
              return this;
          }
  
          /**
           * Set line spacing parameters. Each line will have its line spacing multiplied by
           * {@code spacingMult} and then increased by {@code spacingAdd}. The default is 0.0 for
           * {@code spacingAdd} and 1.0 for {@code spacingMult}.
           *
           * @param spacingAdd the amount of line spacing addition
           * @param spacingMult the line spacing multiplier
           * @return this builder, useful for chaining
           * @see android.widget.TextView#setLineSpacing
           */
          @NonNull
          public Builder setLineSpacing(float spacingAdd, @FloatRange(from = 0.0) float spacingMult) {
              mSpacingAdd = spacingAdd;
              mSpacingMult = spacingMult;
              return this;
          }
  
          /**
           * Set whether to include extra space beyond font ascent and descent (which is
           * needed to avoid clipping in some languages, such as Arabic and Kannada). The
           * default is {@code true}.
           *
           * @param includePad whether to include padding
           * @return this builder, useful for chaining
           * @see android.widget.TextView#setIncludeFontPadding
           */
          @NonNull
          public Builder setIncludePad(boolean includePad) {
              mIncludePad = includePad;
              return this;
          }
  
          /**
           * Set whether to respect the ascent and descent of the fallback fonts that are used in
           * displaying the text (which is needed to avoid text from consecutive lines running into
           * each other). If set, fallback fonts that end up getting used can increase the ascent
           * and descent of the lines that they are used on.
           *
           * <p>For backward compatibility reasons, the default is {@code false}, but setting this to
           * true is strongly recommended. It is required to be true if text could be in languages
           * like Burmese or Tibetan where text is typically much taller or deeper than Latin text.
           *
           * @param useLineSpacingFromFallbacks whether to expand linespacing based on fallback fonts
           * @return this builder, useful for chaining
           */
          @NonNull
          public Builder setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks) {
              mFallbackLineSpacing = useLineSpacingFromFallbacks;
              return this;
          }
  
          /**
           * Set the width as used for ellipsizing purposes, if it differs from the
           * normal layout width. The default is the {@code width}
           * passed to {@link #obtain}.
           *
           * @param ellipsizedWidth width used for ellipsizing, in pixels
           * @return this builder, useful for chaining
           * @see android.widget.TextView#setEllipsize
           */
          @NonNull
          public Builder setEllipsizedWidth(@IntRange(from = 0) int ellipsizedWidth) {
              mEllipsizedWidth = ellipsizedWidth;
              return this;
          }
  
          /**
           * Set ellipsizing on the layout. Causes words that are longer than the view
           * is wide, or exceeding the number of lines (see #setMaxLines) in the case
           * of {@link android.text.TextUtils.TruncateAt#END} or
           * {@link android.text.TextUtils.TruncateAt#MARQUEE}, to be ellipsized instead
           * of broken. The default is {@code null}, indicating no ellipsis is to be applied.
           *
           * @param ellipsize type of ellipsis behavior
           * @return this builder, useful for chaining
           * @see android.widget.TextView#setEllipsize
           */
          @NonNull
          public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) {
              mEllipsize = ellipsize;
              return this;
          }
  
          /**
           * Set maximum number of lines. This is particularly useful in the case of
           * ellipsizing, where it changes the layout of the last line. The default is
           * unlimited.
           *
           * @param maxLines maximum number of lines in the layout
           * @return this builder, useful for chaining
           * @see android.widget.TextView#setMaxLines
           */
          @NonNull
          public Builder setMaxLines(@IntRange(from = 0) int maxLines) {
              mMaxLines = maxLines;
              return this;
          }
  
          /**
           * Set break strategy, useful for selecting high quality or balanced paragraph
           * layout options. The default is {@link Layout#BREAK_STRATEGY_SIMPLE}.
           * <p/>
           * Enabling hyphenation with either using {@link Layout#HYPHENATION_FREQUENCY_NORMAL} or
           * {@link Layout#HYPHENATION_FREQUENCY_FULL} while line breaking is set to one of
           * {@link Layout#BREAK_STRATEGY_BALANCED}, {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}
           * improves the structure of text layout however has performance impact and requires more
           * time to do the text layout.
           *
           * @param breakStrategy break strategy for paragraph layout
           * @return this builder, useful for chaining
           * @see android.widget.TextView#setBreakStrategy
           * @see #setHyphenationFrequency(int)
           */
          @NonNull
          public Builder setBreakStrategy(@BreakStrategy int breakStrategy) {
              mBreakStrategy = breakStrategy;
              return this;
          }
  
          /**
           * Set hyphenation frequency, to control the amount of automatic hyphenation used. The
           * possible values are defined in {@link Layout}, by constants named with the pattern
           * {@code HYPHENATION_FREQUENCY_*}. The default is
           * {@link Layout#HYPHENATION_FREQUENCY_NONE}.
           * <p/>
           * Enabling hyphenation with either using {@link Layout#HYPHENATION_FREQUENCY_NORMAL} or
           * {@link Layout#HYPHENATION_FREQUENCY_FULL} while line breaking is set to one of
           * {@link Layout#BREAK_STRATEGY_BALANCED}, {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}
           * improves the structure of text layout however has performance impact and requires more
           * time to do the text layout.
           *
           * @param hyphenationFrequency hyphenation frequency for the paragraph
           * @return this builder, useful for chaining
           * @see android.widget.TextView#setHyphenationFrequency
           * @see #setBreakStrategy(int)
           */
          @NonNull
          public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) {
              mHyphenationFrequency = hyphenationFrequency;
              return this;
          }
  
          /**
           * Set indents. Arguments are arrays holding an indent amount, one per line, measured in
           * pixels. For lines past the last element in the array, the last element repeats.
           *
           * @param leftIndents array of indent values for left margin, in pixels
           * @param rightIndents array of indent values for right margin, in pixels
           * @return this builder, useful for chaining
           */
          @NonNull
          public Builder setIndents(@Nullable int[] leftIndents, @Nullable int[] rightIndents) {
              mLeftIndents = leftIndents;
              mRightIndents = rightIndents;
              return this;
          }
  
          /**
           * Set paragraph justification mode. The default value is
           * {@link Layout#JUSTIFICATION_MODE_NONE}. If the last line is too short for justification,
           * the last line will be displayed with the alignment set by {@link #setAlignment}.
           * When Justification mode is JUSTIFICATION_MODE_INTER_WORD, wordSpacing on the given
           * {@link Paint} will be ignored. This behavior also affects Spans which change the
           * wordSpacing.
           *
           * @param justificationMode justification mode for the paragraph.
           * @return this builder, useful for chaining.
           * @see Paint#setWordSpacing(float)
           */
          @NonNull
          public Builder setJustificationMode(@JustificationMode int justificationMode) {
              mJustificationMode = justificationMode;
              return this;
          }
  
          /**
           * Sets whether the line spacing should be applied for the last line. Default value is
           * {@code false}.
           *
           * @hide
           */
          @NonNull
          /* package */ Builder setAddLastLineLineSpacing(boolean value) {
              mAddLastLineLineSpacing = value;
              return this;
          }
  
          /**
           * Build the {@link StaticLayout} after options have been set.
           *
           * <p>Note: the builder object must not be reused in any way after calling this
           * method. Setting parameters after calling this method, or calling it a second
           * time on the same builder object, will likely lead to unexpected results.
           *
           * @return the newly constructed {@link StaticLayout} object
           */
          @NonNull
          public StaticLayout build() {
              StaticLayout result = new StaticLayout(this);
              Builder.recycle(this);
              return result;
          }
  
          private CharSequence mText;
          private int mStart;
          private int mEnd;
          private TextPaint mPaint;
          private int mWidth;
          private Alignment mAlignment;
          private TextDirectionHeuristic mTextDir;
          private float mSpacingMult;
          private float mSpacingAdd;
          private boolean mIncludePad;
          private boolean mFallbackLineSpacing;
          private int mEllipsizedWidth;
          private TextUtils.TruncateAt mEllipsize;
          private int mMaxLines;
          private int mBreakStrategy;
          private int mHyphenationFrequency;
          @Nullable private int[] mLeftIndents;
          @Nullable private int[] mRightIndents;
          private int mJustificationMode;
          private boolean mAddLastLineLineSpacing;
  
          private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
  
          private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<>(3);
      }
  
      /**
       * @deprecated Use {@link Builder} instead.
       */
      @Deprecated
      public StaticLayout(CharSequence source, TextPaint paint,
                          int width,
                          Alignment align, float spacingmult, float spacingadd,
                          boolean includepad) {
          this(source, 0, source.length(), paint, width, align,
               spacingmult, spacingadd, includepad);
      }
  
      /**
       * @deprecated Use {@link Builder} instead.
       */
      @Deprecated
      public StaticLayout(CharSequence source, int bufstart, int bufend,
                          TextPaint paint, int outerwidth,
                          Alignment align,
                          float spacingmult, float spacingadd,
                          boolean includepad) {
          this(source, bufstart, bufend, paint, outerwidth, align,
               spacingmult, spacingadd, includepad, null, 0);
      }
  
      /**
       * @deprecated Use {@link Builder} instead.
       */
      @Deprecated
      public StaticLayout(CharSequence source, int bufstart, int bufend,
              TextPaint paint, int outerwidth,
              Alignment align,
              float spacingmult, float spacingadd,
              boolean includepad,
              TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
          this(source, bufstart, bufend, paint, outerwidth, align,
                  TextDirectionHeuristics.FIRSTSTRONG_LTR,
                  spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
      }
  
      /**
       * @hide
       * @deprecated Use {@link Builder} instead.
       */
      @Deprecated
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 117521430)
      public 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) {
          super((ellipsize == null)
                  ? source
                  : (source instanceof Spanned)
                      ? new SpannedEllipsizer(source)
                      : new Ellipsizer(source),
                paint, outerwidth, align, textDir, spacingmult, spacingadd);
  
          Builder b = Builder.obtain(source, bufstart, bufend, paint, outerwidth)
              .setAlignment(align)
              .setTextDirection(textDir)
              .setLineSpacing(spacingadd, spacingmult)
              .setIncludePad(includepad)
              .setEllipsizedWidth(ellipsizedWidth)
              .setEllipsize(ellipsize)
              .setMaxLines(maxLines);
          /*
           * This is annoying, but we can't refer to the layout until superclass construction is
           * finished, and the superclass constructor wants the reference to the display text.
           *
           * In other words, the two Ellipsizer classes in Layout.java need a (Dynamic|Static)Layout
           * as a parameter to do their calculations, but the Ellipsizers also need to be the input
           * to the superclass's constructor (Layout). In order to go around the circular
           * dependency, we construct the Ellipsizer with only one of the parameters, the text. And
           * we fill in the rest of the needed information (layout, width, and method) later, here.
           *
           * This will break if the superclass constructor ever actually cares about the content
           * instead of just holding the reference.
           */
          if (ellipsize != null) {
              Ellipsizer e = (Ellipsizer) getText();
  
              e.mLayout = this;
              e.mWidth = ellipsizedWidth;
              e.mMethod = ellipsize;
              mEllipsizedWidth = ellipsizedWidth;
  
              mColumns = COLUMNS_ELLIPSIZE;
          } else {
              mColumns = COLUMNS_NORMAL;
              mEllipsizedWidth = outerwidth;
          }
  
          mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2);
          mLines  = ArrayUtils.newUnpaddedIntArray(2 * mColumns);
          mMaximumVisibleLineCount = maxLines;
  
          generate(b, b.mIncludePad, b.mIncludePad);
  
          Builder.recycle(b);
      }
  
      /**
       * Used by DynamicLayout.
       */
      /* package */ StaticLayout(@Nullable CharSequence text) {
          super(text, null, 0, null, 0, 0);
  
          mColumns = COLUMNS_ELLIPSIZE;
          mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2);
          mLines  = ArrayUtils.newUnpaddedIntArray(2 * mColumns);
      }
  
      private StaticLayout(Builder b) {
          super((b.mEllipsize == null)
                  ? b.mText
                  : (b.mText instanceof Spanned)
                      ? new SpannedEllipsizer(b.mText)
                      : new Ellipsizer(b.mText),
                  b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd);
  
          if (b.mEllipsize != null) {
              Ellipsizer e = (Ellipsizer) getText();
  
              e.mLayout = this;
              e.mWidth = b.mEllipsizedWidth;
              e.mMethod = b.mEllipsize;
              mEllipsizedWidth = b.mEllipsizedWidth;
  
              mColumns = COLUMNS_ELLIPSIZE;
          } else {
              mColumns = COLUMNS_NORMAL;
              mEllipsizedWidth = b.mWidth;
          }
  
          mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2);
          mLines  = ArrayUtils.newUnpaddedIntArray(2 * mColumns);
          mMaximumVisibleLineCount = b.mMaxLines;
  
          mLeftIndents = b.mLeftIndents;
          mRightIndents = b.mRightIndents;
          setJustificationMode(b.mJustificationMode);
  
          generate(b, b.mIncludePad, b.mIncludePad);
      }
  
      /* package */ void generate(Builder b, boolean includepad, boolean trackpad) {
          final CharSequence source = b.mText;
          final int bufStart = b.mStart;
          final int bufEnd = b.mEnd;
          TextPaint paint = b.mPaint;
          int outerWidth = b.mWidth;
          TextDirectionHeuristic textDir = b.mTextDir;
          final boolean fallbackLineSpacing = b.mFallbackLineSpacing;
          float spacingmult = b.mSpacingMult;
          float spacingadd = b.mSpacingAdd;
          float ellipsizedWidth = b.mEllipsizedWidth;
          TextUtils.TruncateAt ellipsize = b.mEllipsize;
          final boolean addLastLineSpacing = b.mAddLastLineLineSpacing;
  
          int lineBreakCapacity = 0;
          int[] breaks = null;
          float[] lineWidths = null;
          float[] ascents = null;
          float[] descents = null;
          boolean[] hasTabs = null;
          int[] hyphenEdits = null;
  
          mLineCount = 0;
          mEllipsized = false;
          mMaxLineHeight = mMaximumVisibleLineCount < 1 ? 0 : DEFAULT_MAX_LINE_HEIGHT;
  
          int v = 0;
          boolean needMultiply = (spacingmult != 1 || spacingadd != 0);
  
          Paint.FontMetricsInt fm = b.mFontMetricsInt;
          int[] chooseHtv = null;
  
          final int[] indents;
          if (mLeftIndents != null || mRightIndents != null) {
              final int leftLen = mLeftIndents == null ? 0 : mLeftIndents.length;
              final int rightLen = mRightIndents == null ? 0 : mRightIndents.length;
              final int indentsLen = Math.max(leftLen, rightLen);
              indents = new int[indentsLen];
              for (int i = 0; i < leftLen; i++) {
                  indents[i] = mLeftIndents[i];
              }
              for (int i = 0; i < rightLen; i++) {
                  indents[i] += mRightIndents[i];
              }
          } else {
              indents = null;
          }
  
          final LineBreaker lineBreaker = new LineBreaker.Builder()
                  .setBreakStrategy(b.mBreakStrategy)
                  .setHyphenationFrequency(b.mHyphenationFrequency)
                  // TODO: Support more justification mode, e.g. letter spacing, stretching.
                  .setJustificationMode(b.mJustificationMode)
                  .setIndents(indents)
                  .build();
  
          LineBreaker.ParagraphConstraints constraints =
                  new LineBreaker.ParagraphConstraints();
  
          PrecomputedText.ParagraphInfo[] paragraphInfo = null;
          final Spanned spanned = (source instanceof Spanned) ? (Spanned) source : null;
          if (source instanceof PrecomputedText) {
              PrecomputedText precomputed = (PrecomputedText) source;
              final @PrecomputedText.Params.CheckResultUsableResult int checkResult =
                      precomputed.checkResultUsable(bufStart, bufEnd, textDir, paint,
                              b.mBreakStrategy, b.mHyphenationFrequency);
              switch (checkResult) {
                  case PrecomputedText.Params.UNUSABLE:
                      break;
                  case PrecomputedText.Params.NEED_RECOMPUTE:
                      final PrecomputedText.Params newParams =
                              new PrecomputedText.Params.Builder(paint)
                                  .setBreakStrategy(b.mBreakStrategy)
                                  .setHyphenationFrequency(b.mHyphenationFrequency)
                                  .setTextDirection(textDir)
                                  .build();
                      precomputed = PrecomputedText.create(precomputed, newParams);
                      paragraphInfo = precomputed.getParagraphInfo();
                      break;
                  case PrecomputedText.Params.USABLE:
                      // Some parameters are different from the ones when measured text is created.
                      paragraphInfo = precomputed.getParagraphInfo();
                      break;
              }
          }
  
          if (paragraphInfo == null) {
              final PrecomputedText.Params param = new PrecomputedText.Params(paint, textDir,
                      b.mBreakStrategy, b.mHyphenationFrequency);
              paragraphInfo = PrecomputedText.createMeasuredParagraphs(source, param, bufStart,
                      bufEnd, false /* computeLayout */);
          }
  
          for (int paraIndex = 0; paraIndex < paragraphInfo.length; paraIndex++) {
              final int paraStart = paraIndex == 0
                      ? bufStart : paragraphInfo[paraIndex - 1].paragraphEnd;
              final int paraEnd = paragraphInfo[paraIndex].paragraphEnd;
  
              int firstWidthLineCount = 1;
              int firstWidth = outerWidth;
              int restWidth = outerWidth;
  
              LineHeightSpan[] chooseHt = null;
              if (spanned != null) {
                  LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd,
                          LeadingMarginSpan.class);
                  for (int i = 0; i < sp.length; i++) {
                      LeadingMarginSpan lms = sp[i];
                      firstWidth -= sp[i].getLeadingMargin(true);
                      restWidth -= sp[i].getLeadingMargin(false);
  
                      // LeadingMarginSpan2 is odd.  The count affects all
                      // leading margin spans, not just this particular one
                      if (lms instanceof LeadingMarginSpan2) {
                          LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms;
                          firstWidthLineCount = Math.max(firstWidthLineCount,
                                  lms2.getLeadingMarginLineCount());
                      }
                  }
  
                  chooseHt = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class);
  
                  if (chooseHt.length == 0) {
                      chooseHt = null; // So that out() would not assume it has any contents
                  } else {
                      if (chooseHtv == null || chooseHtv.length < chooseHt.length) {
                          chooseHtv = ArrayUtils.newUnpaddedIntArray(chooseHt.length);
                      }
  
                      for (int i = 0; i < chooseHt.length; i++) {
                          int o = spanned.getSpanStart(chooseHt[i]);
  
                          if (o < paraStart) {
                              // starts in this layout, before the
                              // current paragraph
  
                              chooseHtv[i] = getLineTop(getLineForOffset(o));
                          } else {
                              // starts in this paragraph
  
                              chooseHtv[i] = v;
                          }
                      }
                  }
              }
              // tab stop locations
              float[] variableTabStops = null;
              if (spanned != null) {
                  TabStopSpan[] spans = getParagraphSpans(spanned, paraStart,
                          paraEnd, TabStopSpan.class);
                  if (spans.length > 0) {
                      float[] stops = new float[spans.length];
                      for (int i = 0; i < spans.length; i++) {
                          stops[i] = (float) spans[i].getTabStop();
                      }
                      Arrays.sort(stops, 0, stops.length);
                      variableTabStops = stops;
                  }
              }
  
              final MeasuredParagraph measuredPara = paragraphInfo[paraIndex].measured;
              final char[] chs = measuredPara.getChars();
              final int[] spanEndCache = measuredPara.getSpanEndCache().getRawArray();
              final int[] fmCache = measuredPara.getFontMetrics().getRawArray();
  
              constraints.setWidth(restWidth);
              constraints.setIndent(firstWidth, firstWidthLineCount);
              constraints.setTabStops(variableTabStops, TAB_INCREMENT);
  
              LineBreaker.Result res = lineBreaker.computeLineBreaks(
                      measuredPara.getMeasuredText(), constraints, mLineCount);
              int breakCount = res.getLineCount();
              if (lineBreakCapacity < breakCount) {
                  lineBreakCapacity = breakCount;
                  breaks = new int[lineBreakCapacity];
                  lineWidths = new float[lineBreakCapacity];
                  ascents = new float[lineBreakCapacity];
                  descents = new float[lineBreakCapacity];
                  hasTabs = new boolean[lineBreakCapacity];
                  hyphenEdits = new int[lineBreakCapacity];
              }
  
              for (int i = 0; i < breakCount; ++i) {
                  breaks[i] = res.getLineBreakOffset(i);
                  lineWidths[i] = res.getLineWidth(i);
                  ascents[i] = res.getLineAscent(i);
                  descents[i] = res.getLineDescent(i);
                  hasTabs[i] = res.hasLineTab(i);
                  hyphenEdits[i] =
                      packHyphenEdit(res.getStartLineHyphenEdit(i), res.getEndLineHyphenEdit(i));
              }
  
              final int remainingLineCount = mMaximumVisibleLineCount - mLineCount;
              final boolean ellipsisMayBeApplied = ellipsize != null
                      && (ellipsize == TextUtils.TruncateAt.END
                          || (mMaximumVisibleLineCount == 1
                                  && ellipsize != TextUtils.TruncateAt.MARQUEE));
              if (0 < remainingLineCount && remainingLineCount < breakCount
                      && ellipsisMayBeApplied) {
                  // Calculate width
                  float width = 0;
                  boolean hasTab = false;  // XXX May need to also have starting hyphen edit
                  for (int i = remainingLineCount - 1; i < breakCount; i++) {
                      if (i == breakCount - 1) {
                          width += lineWidths[i];
                      } else {
                          for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) {
                              width += measuredPara.getCharWidthAt(j);
                          }
                      }
                      hasTab |= hasTabs[i];
                  }
                  // Treat the last line and overflowed lines as a single line.
                  breaks[remainingLineCount - 1] = breaks[breakCount - 1];
                  lineWidths[remainingLineCount - 1] = width;
                  hasTabs[remainingLineCount - 1] = hasTab;
  
                  breakCount = remainingLineCount;
              }
  
              // here is the offset of the starting character of the line we are currently
              // measuring
              int here = paraStart;
  
              int fmTop = 0, fmBottom = 0, fmAscent = 0, fmDescent = 0;
              int fmCacheIndex = 0;
              int spanEndCacheIndex = 0;
              int breakIndex = 0;
              for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
                  // retrieve end of span
                  spanEnd = spanEndCache[spanEndCacheIndex++];
  
                  // retrieve cached metrics, order matches above
                  fm.top = fmCache[fmCacheIndex * 4 + 0];
                  fm.bottom = fmCache[fmCacheIndex * 4 + 1];
                  fm.ascent = fmCache[fmCacheIndex * 4 + 2];
                  fm.descent = fmCache[fmCacheIndex * 4 + 3];
                  fmCacheIndex++;
  
                  if (fm.top < fmTop) {
                      fmTop = fm.top;
                  }
                  if (fm.ascent < fmAscent) {
                      fmAscent = fm.ascent;
                  }
                  if (fm.descent > fmDescent) {
                      fmDescent = fm.descent;
                  }
                  if (fm.bottom > fmBottom) {
                      fmBottom = fm.bottom;
                  }
  
                  // skip breaks ending before current span range
                  while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) {
                      breakIndex++;
                  }
  
                  while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) {
                      int endPos = paraStart + breaks[breakIndex];
  
                      boolean moreChars = (endPos < bufEnd);
  
                      final int ascent = fallbackLineSpacing
                              ? Math.min(fmAscent, Math.round(ascents[breakIndex]))
                              : fmAscent;
                      final int descent = fallbackLineSpacing
                              ? Math.max(fmDescent, Math.round(descents[breakIndex]))
                              : fmDescent;
  
                      v = out(source, here, endPos,
                              ascent, descent, fmTop, fmBottom,
                              v, spacingmult, spacingadd, chooseHt, chooseHtv, fm,
                              hasTabs[breakIndex], hyphenEdits[breakIndex], needMultiply,
                              measuredPara, bufEnd, includepad, trackpad, addLastLineSpacing, chs,
                              paraStart, ellipsize, ellipsizedWidth, lineWidths[breakIndex],
                              paint, moreChars);
  
                      if (endPos < spanEnd) {
                          // preserve metrics for current span
                          fmTop = fm.top;
                          fmBottom = fm.bottom;
                          fmAscent = fm.ascent;
                          fmDescent = fm.descent;
                      } else {
                          fmTop = fmBottom = fmAscent = fmDescent = 0;
                      }
  
                      here = endPos;
                      breakIndex++;
  
                      if (mLineCount >= mMaximumVisibleLineCount && mEllipsized) {
                          return;
                      }
                  }
              }
  
              if (paraEnd == bufEnd) {
                  break;
              }
          }
  
          if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE)
                  && mLineCount < mMaximumVisibleLineCount) {
              final MeasuredParagraph measuredPara =
                      MeasuredParagraph.buildForBidi(source, bufEnd, bufEnd, textDir, null);
              paint.getFontMetricsInt(fm);
              v = out(source,
                      bufEnd, bufEnd, fm.ascent, fm.descent,
                      fm.top, fm.bottom,
                      v,
                      spacingmult, spacingadd, null,
                      null, fm, false, 0,
                      needMultiply, measuredPara, bufEnd,
                      includepad, trackpad, addLastLineSpacing, null,
                      bufStart, ellipsize,
                      ellipsizedWidth, 0, paint, false);
          }
      }
  
      private int out(final CharSequence text, final int start, final int end, int above, int below,
              int top, int bottom, int v, final float spacingmult, final float spacingadd,
              final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm,
              final boolean hasTab, final int hyphenEdit, final boolean needMultiply,
              @NonNull final MeasuredParagraph measured,
              final int bufEnd, final boolean includePad, final boolean trackPad,
              final boolean addLastLineLineSpacing, final char[] chs,
              final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth,
              final float textWidth, final TextPaint paint, final boolean moreChars) {
          final int j = mLineCount;
          final int off = j * mColumns;
          final int want = off + mColumns + TOP;
          int[] lines = mLines;
          final int dir = measured.getParagraphDir();
  
          if (want >= lines.length) {
              final int[] grow = ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(want));
              System.arraycopy(lines, 0, grow, 0, lines.length);
              mLines = grow;
              lines = grow;
          }
  
          if (j >= mLineDirections.length) {
              final Directions[] grow = ArrayUtils.newUnpaddedArray(Directions.class,
                      GrowingArrayUtils.growSize(j));
              System.arraycopy(mLineDirections, 0, grow, 0, mLineDirections.length);
              mLineDirections = grow;
          }
  
          if (chooseHt != null) {
              fm.ascent = above;
              fm.descent = below;
              fm.top = top;
              fm.bottom = bottom;
  
              for (int i = 0; i < chooseHt.length; i++) {
                  if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
                      ((LineHeightSpan.WithDensity) chooseHt[i])
                              .chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
                  } else {
                      chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
                  }
              }
  
              above = fm.ascent;
              below = fm.descent;
              top = fm.top;
              bottom = fm.bottom;
          }
  
          boolean firstLine = (j == 0);
          boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
  
          if (ellipsize != null) {
              // If there is only one line, then do any type of ellipsis except when it is MARQUEE
              // if there are multiple lines, just allow END ellipsis on the last line
              boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
  
              boolean doEllipsis =
                      (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) &&
                              ellipsize != TextUtils.TruncateAt.MARQUEE) ||
                      (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
                              ellipsize == TextUtils.TruncateAt.END);
              if (doEllipsis) {
                  calculateEllipsis(start, end, measured, widthStart,
                          ellipsisWidth, ellipsize, j,
                          textWidth, paint, forceEllipsis);
              }
          }
  
          final boolean lastLine;
          if (mEllipsized) {
              lastLine = true;
          } else {
              final boolean lastCharIsNewLine = widthStart != bufEnd && bufEnd > 0
                      && text.charAt(bufEnd - 1) == CHAR_NEW_LINE;
              if (end == bufEnd && !lastCharIsNewLine) {
                  lastLine = true;
              } else if (start == bufEnd && lastCharIsNewLine) {
                  lastLine = true;
              } else {
                  lastLine = false;
              }
          }
  
          if (firstLine) {
              if (trackPad) {
                  mTopPadding = top - above;
              }
  
              if (includePad) {
                  above = top;
              }
          }
  
          int extra;
  
          if (lastLine) {
              if (trackPad) {
                  mBottomPadding = bottom - below;
              }
  
              if (includePad) {
                  below = bottom;
              }
          }
  
          if (needMultiply && (addLastLineLineSpacing || !lastLine)) {
              double ex = (below - above) * (spacingmult - 1) + spacingadd;
              if (ex >= 0) {
                  extra = (int)(ex + EXTRA_ROUNDING);
              } else {
                  extra = -(int)(-ex + EXTRA_ROUNDING);
              }
          } else {
              extra = 0;
          }
  
          lines[off + START] = start;
          lines[off + TOP] = v;
          lines[off + DESCENT] = below + extra;
          lines[off + EXTRA] = extra;
  
          // special case for non-ellipsized last visible line when maxLines is set
          // store the height as if it was ellipsized
          if (!mEllipsized && currentLineIsTheLastVisibleOne) {
              // below calculation as if it was the last line
              int maxLineBelow = includePad ? bottom : below;
              // similar to the calculation of v below, without the extra.
              mMaxLineHeight = v + (maxLineBelow - above);
          }
  
          v += (below - above) + extra;
          lines[off + mColumns + START] = end;
          lines[off + mColumns + TOP] = v;
  
          // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining
          // one bit for start field
          lines[off + TAB] |= hasTab ? TAB_MASK : 0;
          lines[off + HYPHEN] = hyphenEdit;
          lines[off + DIR] |= dir << DIR_SHIFT;
          mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart);
  
          mLineCount++;
          return v;
      }
  
      private void calculateEllipsis(int lineStart, int lineEnd,
                                     MeasuredParagraph measured, int widthStart,
                                     float avail, TextUtils.TruncateAt where,
                                     int line, float textWidth, TextPaint paint,
                                     boolean forceEllipsis) {
          avail -= getTotalInsets(line);
          if (textWidth <= avail && !forceEllipsis) {
              // Everything fits!
              mLines[mColumns * line + ELLIPSIS_START] = 0;
              mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
              return;
          }
  
          float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where));
          int ellipsisStart = 0;
          int ellipsisCount = 0;
          int len = lineEnd - lineStart;
  
          // We only support start ellipsis on a single line
          if (where == TextUtils.TruncateAt.START) {
              if (mMaximumVisibleLineCount == 1) {
                  float sum = 0;
                  int i;
  
                  for (i = len; i > 0; i--) {
                      float w = measured.getCharWidthAt(i - 1 + lineStart - widthStart);
                      if (w + sum + ellipsisWidth > avail) {
                          while (i < len
                                  && measured.getCharWidthAt(i + lineStart - widthStart) == 0.0f) {
                              i++;
                          }
                          break;
                      }
  
                      sum += w;
                  }
  
                  ellipsisStart = 0;
                  ellipsisCount = i;
              } else {
                  if (Log.isLoggable(TAG, Log.WARN)) {
                      Log.w(TAG, "Start Ellipsis only supported with one line");
                  }
              }
          } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
                  where == TextUtils.TruncateAt.END_SMALL) {
              float sum = 0;
              int i;
  
              for (i = 0; i < len; i++) {
                  float w = measured.getCharWidthAt(i + lineStart - widthStart);
  
                  if (w + sum + ellipsisWidth > avail) {
                      break;
                  }
  
                  sum += w;
              }
  
              ellipsisStart = i;
              ellipsisCount = len - i;
              if (forceEllipsis && ellipsisCount == 0 && len > 0) {
                  ellipsisStart = len - 1;
                  ellipsisCount = 1;
              }
          } else {
              // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line
              if (mMaximumVisibleLineCount == 1) {
                  float lsum = 0, rsum = 0;
                  int left = 0, right = len;
  
                  float ravail = (avail - ellipsisWidth) / 2;
                  for (right = len; right > 0; right--) {
                      float w = measured.getCharWidthAt(right - 1 + lineStart - widthStart);
  
                      if (w + rsum > ravail) {
                          while (right < len
                                  && measured.getCharWidthAt(right + lineStart - widthStart)
                                      == 0.0f) {
                              right++;
                          }
                          break;
                      }
                      rsum += w;
                  }
  
                  float lavail = avail - ellipsisWidth - rsum;
                  for (left = 0; left < right; left++) {
                      float w = measured.getCharWidthAt(left + lineStart - widthStart);
  
                      if (w + lsum > lavail) {
                          break;
                      }
  
                      lsum += w;
                  }
  
                  ellipsisStart = left;
                  ellipsisCount = right - left;
              } else {
                  if (Log.isLoggable(TAG, Log.WARN)) {
                      Log.w(TAG, "Middle Ellipsis only supported with one line");
                  }
              }
          }
          mEllipsized = true;
          mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
          mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
      }
  
      private float getTotalInsets(int line) {
          int totalIndent = 0;
          if (mLeftIndents != null) {
              totalIndent = mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
          }
          if (mRightIndents != null) {
              totalIndent += mRightIndents[Math.min(line, mRightIndents.length - 1)];
          }
          return totalIndent;
      }
  
      // Override the base class so we can directly access our members,
      // rather than relying on member functions.
      // The logic mirrors that of Layout.getLineForVertical
      // FIXME: It may be faster to do a linear search for layouts without many lines.
      @Override
      public int getLineForVertical(int vertical) {
          int high = mLineCount;
          int low = -1;
          int guess;
          int[] lines = mLines;
          while (high - low > 1) {
              guess = (high + low) >> 1;
              if (lines[mColumns * guess + TOP] > vertical){
                  high = guess;
              } else {
                  low = guess;
              }
          }
          if (low < 0) {
              return 0;
          } else {
              return low;
          }
      }
  
      @Override
      public int getLineCount() {
          return mLineCount;
      }
  
      @Override
      public int getLineTop(int line) {
          return mLines[mColumns * line + TOP];
      }
  
      /**
       * @hide
       */
      @Override
      public int getLineExtra(int line) {
          return mLines[mColumns * line + EXTRA];
      }
  
      @Override
      public int getLineDescent(int line) {
          return mLines[mColumns * line + DESCENT];
      }
  
      @Override
      public int getLineStart(int line) {
          return mLines[mColumns * line + START] & START_MASK;
      }
  
      @Override
      public int getParagraphDirection(int line) {
          return mLines[mColumns * line + DIR] >> DIR_SHIFT;
      }
  
      @Override
      public boolean getLineContainsTab(int line) {
          return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
      }
  
      @Override
      public final Directions getLineDirections(int line) {
          if (line > getLineCount()) {
              throw new ArrayIndexOutOfBoundsException();
          }
          return mLineDirections[line];
      }
  
      @Override
      public int getTopPadding() {
          return mTopPadding;
      }
  
      @Override
      public int getBottomPadding() {
          return mBottomPadding;
      }
  
      // To store into single int field, pack the pair of start and end hyphen edit.
      static int packHyphenEdit(
              @Paint.StartHyphenEdit int start, @Paint.EndHyphenEdit int end) {
          return start << START_HYPHEN_BITS_SHIFT | end;
      }
  
      static int unpackStartHyphenEdit(int packedHyphenEdit) {
          return (packedHyphenEdit & START_HYPHEN_MASK) >> START_HYPHEN_BITS_SHIFT;
      }
  
      static int unpackEndHyphenEdit(int packedHyphenEdit) {
          return packedHyphenEdit & END_HYPHEN_MASK;
      }
  
      /**
       * Returns the start hyphen edit value for this line.
       *
       * @param lineNumber a line number
       * @return A start hyphen edit value.
       * @hide
       */
      @Override
      public @Paint.StartHyphenEdit int getStartHyphenEdit(int lineNumber) {
          return unpackStartHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK);
      }
  
      /**
       * Returns the packed hyphen edit value for this line.
       *
       * @param lineNumber a line number
       * @return An end hyphen edit value.
       * @hide
       */
      @Override
      public @Paint.EndHyphenEdit int getEndHyphenEdit(int lineNumber) {
          return unpackEndHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK);
      }
  
      /**
       * @hide
       */
      @Override
      public int getIndentAdjust(int line, Alignment align) {
          if (align == Alignment.ALIGN_LEFT) {
              if (mLeftIndents == null) {
                  return 0;
              } else {
                  return mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
              }
          } else if (align == Alignment.ALIGN_RIGHT) {
              if (mRightIndents == null) {
                  return 0;
              } else {
                  return -mRightIndents[Math.min(line, mRightIndents.length - 1)];
              }
          } else if (align == Alignment.ALIGN_CENTER) {
              int left = 0;
              if (mLeftIndents != null) {
                  left = mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
              }
              int right = 0;
              if (mRightIndents != null) {
                  right = mRightIndents[Math.min(line, mRightIndents.length - 1)];
              }
              return (left - right) >> 1;
          } else {
              throw new AssertionError("unhandled alignment " + align);
          }
      }
  
      @Override
      public int getEllipsisCount(int line) {
          if (mColumns < COLUMNS_ELLIPSIZE) {
              return 0;
          }
  
          return mLines[mColumns * line + ELLIPSIS_COUNT];
      }
  
      @Override
      public int getEllipsisStart(int line) {
          if (mColumns < COLUMNS_ELLIPSIZE) {
              return 0;
          }
  
          return mLines[mColumns * line + ELLIPSIS_START];
      }
  
      @Override
      public int getEllipsizedWidth() {
          return mEllipsizedWidth;
      }
  
      /**
       * Return the total height of this layout.
       *
       * @param cap if true and max lines is set, returns the height of the layout at the max lines.
       *
       * @hide
       */
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
      public int getHeight(boolean cap) {
          if (cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight == -1
                  && Log.isLoggable(TAG, Log.WARN)) {
              Log.w(TAG, "maxLineHeight should not be -1. "
                      + " maxLines:" + mMaximumVisibleLineCount
                      + " lineCount:" + mLineCount);
          }
  
          return cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight != -1
                  ? mMaxLineHeight : super.getHeight();
      }
  
      @UnsupportedAppUsage
      private int mLineCount;
      private int mTopPadding, mBottomPadding;
      @UnsupportedAppUsage
      private int mColumns;
      private int mEllipsizedWidth;
  
      /**
       * Keeps track if ellipsize is applied to the text.
       */
      private boolean mEllipsized;
  
      /**
       * If maxLines is set, ellipsize is not set, and the actual line count of text is greater than
       * or equal to maxLine, this variable holds the ideal visual height of the maxLine'th line
       * starting from the top of the layout. If maxLines is not set its value will be -1.
       *
       * The value is the same as getLineTop(maxLines) for ellipsized version where structurally no
       * more than maxLines is contained.
       */
      private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT;
  
      private static final int COLUMNS_NORMAL = 5;
      private static final int COLUMNS_ELLIPSIZE = 7;
      private static final int START = 0;
      private static final int DIR = START;
      private static final int TAB = START;
      private static final int TOP = 1;
      private static final int DESCENT = 2;
      private static final int EXTRA = 3;
      private static final int HYPHEN = 4;
      @UnsupportedAppUsage
      private static final int ELLIPSIS_START = 5;
      private static final int ELLIPSIS_COUNT = 6;
  
      @UnsupportedAppUsage
      private int[] mLines;
      @UnsupportedAppUsage
      private Directions[] mLineDirections;
      @UnsupportedAppUsage
      private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
  
      private static final int START_MASK = 0x1FFFFFFF;
      private static final int DIR_SHIFT  = 30;
      private static final int TAB_MASK   = 0x20000000;
      private static final int HYPHEN_MASK = 0xFF;
      private static final int START_HYPHEN_BITS_SHIFT = 3;
      private static final int START_HYPHEN_MASK = 0x18; // 0b11000
      private static final int END_HYPHEN_MASK = 0x7;  // 0b00111
  
      private static final float TAB_INCREMENT = 20; // same as Layout, but that's private
  
      private static final char CHAR_NEW_LINE = '\n';
  
      private static final double EXTRA_ROUNDING = 0.5;
  
      private static final int DEFAULT_MAX_LINE_HEIGHT = -1;
  
      // Unused, here because of gray list private API accesses.
      /*package*/ static class LineBreaks {
          private static final int INITIAL_SIZE = 16;
          @UnsupportedAppUsage
          public int[] breaks = new int[INITIAL_SIZE];
          @UnsupportedAppUsage
          public float[] widths = new float[INITIAL_SIZE];
          @UnsupportedAppUsage
          public float[] ascents = new float[INITIAL_SIZE];
          @UnsupportedAppUsage
          public float[] descents = new float[INITIAL_SIZE];
          @UnsupportedAppUsage
          public int[] flags = new int[INITIAL_SIZE]; // hasTab
          // breaks, widths, and flags should all have the same length
      }
  
      @Nullable private int[] mLeftIndents;
      @Nullable private int[] mRightIndents;
  }
  