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.UnsupportedAppUsage;
  import android.graphics.Canvas;
  import android.graphics.Paint;
  import android.graphics.Path;
  import android.text.style.ParagraphStyle;
  
  /**
   * A BoringLayout is a very simple Layout implementation for text that
   * fits on a single line and is all left-to-right characters.
   * You will probably never want to make one of these yourself;
   * if you do, be sure to call {@link #isBoring} first to make sure
   * the text meets the criteria.
   * <p>This class 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, in which case
   * you are encouraged to use a Layout instead of calling
   * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
   *  Canvas.drawText()} directly.</p>
   */
  public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback {
  
      /**
       * Utility function to construct a BoringLayout instance.
       *
       * @param source the text to render
       * @param paint the default paint for the layout
       * @param outerWidth the wrapping width for the text
       * @param align whether to left, right, or center the text
       * @param spacingMult this value is no longer used by BoringLayout
       * @param spacingAdd this value is no longer used by BoringLayout
       * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
       *                line width
       * @param includePad set whether to include extra space beyond font ascent and descent which is
       *                   needed to avoid clipping in some scripts
       */
      public static BoringLayout make(CharSequence source, TextPaint paint, int outerWidth,
              Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics,
              boolean includePad) {
          return new BoringLayout(source, paint, outerWidth, align, spacingMult, spacingAdd, metrics,
                  includePad);
      }
  
      /**
       * Utility function to construct a BoringLayout instance.
       *
       * @param source the text to render
       * @param paint the default paint for the layout
       * @param outerWidth the wrapping width for the text
       * @param align whether to left, right, or center the text
       * @param spacingmult this value is no longer used by BoringLayout
       * @param spacingadd this value is no longer used by BoringLayout
       * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
       *                line width
       * @param includePad set whether to include extra space beyond font ascent and descent which is
       *                   needed to avoid clipping in some scripts
       * @param ellipsize whether to ellipsize the text if width of the text is longer than the
       *                  requested width
       * @param ellipsizedWidth the width to which this Layout is ellipsizing. If {@code ellipsize} is
       *                        {@code null}, or is {@link TextUtils.TruncateAt#MARQUEE} this value is
       *                        not used, {@code outerWidth} is used instead
       */
      public static BoringLayout make(CharSequence source, TextPaint paint, int outerWidth,
              Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics,
              boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
          return new BoringLayout(source, paint, outerWidth, align, spacingmult, spacingadd, metrics,
                  includePad, ellipsize, ellipsizedWidth);
      }
  
      /**
       * Returns a BoringLayout for the specified text, potentially reusing
       * this one if it is already suitable.  The caller must make sure that
       * no one is still using this Layout.
       *
       * @param source the text to render
       * @param paint the default paint for the layout
       * @param outerwidth the wrapping width for the text
       * @param align whether to left, right, or center the text
       * @param spacingMult this value is no longer used by BoringLayout
       * @param spacingAdd this value is no longer used by BoringLayout
       * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
       *                line width
       * @param includePad set whether to include extra space beyond font ascent and descent which is
       *                   needed to avoid clipping in some scripts
       */
      public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, int outerwidth,
              Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics,
              boolean includePad) {
          replaceWith(source, paint, outerwidth, align, spacingMult, spacingAdd);
  
          mEllipsizedWidth = outerwidth;
          mEllipsizedStart = 0;
          mEllipsizedCount = 0;
  
          init(source, paint, align, metrics, includePad, true);
          return this;
      }
  
      /**
       * Returns a BoringLayout for the specified text, potentially reusing
       * this one if it is already suitable.  The caller must make sure that
       * no one is still using this Layout.
       *
       * @param source the text to render
       * @param paint the default paint for the layout
       * @param outerWidth the wrapping width for the text
       * @param align whether to left, right, or center the text
       * @param spacingMult this value is no longer used by BoringLayout
       * @param spacingAdd this value is no longer used by BoringLayout
       * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
       *                line width
       * @param includePad set whether to include extra space beyond font ascent and descent which is
       *                   needed to avoid clipping in some scripts
       * @param ellipsize whether to ellipsize the text if width of the text is longer than the
       *                  requested width
       * @param ellipsizedWidth the width to which this Layout is ellipsizing. If {@code ellipsize} is
       *                        {@code null}, or is {@link TextUtils.TruncateAt#MARQUEE} this value is
       *                        not used, {@code outerwidth} is used instead
       */
      public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, int outerWidth,
              Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics,
              boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
          boolean trust;
  
          if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {
              replaceWith(source, paint, outerWidth, align, spacingMult, spacingAdd);
  
              mEllipsizedWidth = outerWidth;
              mEllipsizedStart = 0;
              mEllipsizedCount = 0;
              trust = true;
          } else {
              replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true, this),
                      paint, outerWidth, align, spacingMult, spacingAdd);
  
              mEllipsizedWidth = ellipsizedWidth;
              trust = false;
          }
  
          init(getText(), paint, align, metrics, includePad, trust);
          return this;
      }
  
      /**
       * @param source the text to render
       * @param paint the default paint for the layout
       * @param outerwidth the wrapping width for the text
       * @param align whether to left, right, or center the text
       * @param spacingMult this value is no longer used by BoringLayout
       * @param spacingAdd this value is no longer used by BoringLayout
       * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
       *                line width
       * @param includePad set whether to include extra space beyond font ascent and descent which is
       *                   needed to avoid clipping in some scripts
       */
      public BoringLayout(CharSequence source, TextPaint paint, int outerwidth, Alignment align,
              float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad) {
          super(source, paint, outerwidth, align, spacingMult, spacingAdd);
  
          mEllipsizedWidth = outerwidth;
          mEllipsizedStart = 0;
          mEllipsizedCount = 0;
  
          init(source, paint, align, metrics, includePad, true);
      }
  
      /**
       *
       * @param source the text to render
       * @param paint the default paint for the layout
       * @param outerWidth the wrapping width for the text
       * @param align whether to left, right, or center the text
       * @param spacingMult this value is no longer used by BoringLayout
       * @param spacingAdd this value is no longer used by BoringLayout
       * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
       *                line width
       * @param includePad set whether to include extra space beyond font ascent and descent which is
       *                   needed to avoid clipping in some scripts
       * @param ellipsize whether to ellipsize the text if width of the text is longer than the
       *                  requested {@code outerwidth}
       * @param ellipsizedWidth the width to which this Layout is ellipsizing. If {@code ellipsize} is
       *                        {@code null}, or is {@link TextUtils.TruncateAt#MARQUEE} this value is
       *                        not used, {@code outerwidth} is used instead
       */
      public BoringLayout(CharSequence source, TextPaint paint, int outerWidth, Alignment align,
              float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad,
              TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
          /*
           * It is silly to have to call super() and then replaceWith(),
           * but we can't use "this" for the callback until the call to
           * super() finishes.
           */
          super(source, paint, outerWidth, align, spacingMult, spacingAdd);
  
          boolean trust;
  
          if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {
              mEllipsizedWidth = outerWidth;
              mEllipsizedStart = 0;
              mEllipsizedCount = 0;
              trust = true;
          } else {
              replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true, this),
                          paint, outerWidth, align, spacingMult, spacingAdd);
  
              mEllipsizedWidth = ellipsizedWidth;
              trust = false;
          }
  
          init(getText(), paint, align, metrics, includePad, trust);
      }
  
      /* package */ void init(CharSequence source, TextPaint paint, Alignment align,
              BoringLayout.Metrics metrics, boolean includePad, boolean trustWidth) {
          int spacing;
  
          if (source instanceof String && align == Layout.Alignment.ALIGN_NORMAL) {
              mDirect = source.toString();
          } else {
              mDirect = null;
          }
  
          mPaint = paint;
  
          if (includePad) {
              spacing = metrics.bottom - metrics.top;
              mDesc = metrics.bottom;
          } else {
              spacing = metrics.descent - metrics.ascent;
              mDesc = metrics.descent;
          }
  
          mBottom = spacing;
  
          if (trustWidth) {
              mMax = metrics.width;
          } else {
              /*
               * If we have ellipsized, we have to actually calculate the
               * width because the width that was passed in was for the
               * full text, not the ellipsized form.
               */
              TextLine line = TextLine.obtain();
              line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT,
                      Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null,
                      mEllipsizedStart, mEllipsizedStart + mEllipsizedCount);
              mMax = (int) Math.ceil(line.metrics(null));
              TextLine.recycle(line);
          }
  
          if (includePad) {
              mTopPadding = metrics.top - metrics.ascent;
              mBottomPadding = metrics.bottom - metrics.descent;
          }
      }
  
      /**
       * Determine and compute metrics if given text can be handled by BoringLayout.
       *
       * @param text a text
       * @param paint a paint
       * @return layout metric for the given text. null if given text is unable to be handled by
       *         BoringLayout.
       */
      public static Metrics isBoring(CharSequence text, TextPaint paint) {
          return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, null);
      }
  
      /**
       * Determine and compute metrics if given text can be handled by BoringLayout.
       *
       * @param text a text
       * @param paint a paint
       * @param metrics a metrics object to be recycled. If null is passed, this function creat new
       *                object.
       * @return layout metric for the given text. If metrics is not null, this method fills values
       *         to given metrics object instead of allocating new metrics object. null if given text
       *         is unable to be handled by BoringLayout.
       */
      public static Metrics isBoring(CharSequence text, TextPaint paint, Metrics metrics) {
          return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, metrics);
      }
  
      /**
       * Returns true if the text contains any RTL characters, bidi format characters, or surrogate
       * code units.
       */
      private static boolean hasAnyInterestingChars(CharSequence text, int textLength) {
          final int MAX_BUF_LEN = 500;
          final char[] buffer = TextUtils.obtain(MAX_BUF_LEN);
          try {
              for (int start = 0; start < textLength; start += MAX_BUF_LEN) {
                  final int end = Math.min(start + MAX_BUF_LEN, textLength);
  
                  // No need to worry about getting half codepoints, since we consider surrogate code
                  // units "interesting" as soon we see one.
                  TextUtils.getChars(text, start, end, buffer, 0);
  
                  final int len = end - start;
                  for (int i = 0; i < len; i++) {
                      final char c = buffer[i];
                      if (c == '\n' || c == '\t' || TextUtils.couldAffectRtl(c)) {
                          return true;
                      }
                  }
              }
              return false;
          } finally {
              TextUtils.recycle(buffer);
          }
      }
  
      /**
       * Returns null if not boring; the width, ascent, and descent in the
       * provided Metrics object (or a new one if the provided one was null)
       * if boring.
       * @hide
       */
      @UnsupportedAppUsage
      public static Metrics isBoring(CharSequence text, TextPaint paint,
              TextDirectionHeuristic textDir, Metrics metrics) {
          final int textLength = text.length();
          if (hasAnyInterestingChars(text, textLength)) {
             return null;  // There are some interesting characters. Not boring.
          }
          if (textDir != null && textDir.isRtl(text, 0, textLength)) {
             return null;  // The heuristic considers the whole text RTL. Not boring.
          }
          if (text instanceof Spanned) {
              Spanned sp = (Spanned) text;
              Object[] styles = sp.getSpans(0, textLength, ParagraphStyle.class);
              if (styles.length > 0) {
                  return null;  // There are some ParagraphStyle spans. Not boring.
              }
          }
  
          Metrics fm = metrics;
          if (fm == null) {
              fm = new Metrics();
          } else {
              fm.reset();
          }
  
          TextLine line = TextLine.obtain();
          line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT,
                  Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null,
                  0 /* ellipsisStart, 0 since text has not been ellipsized at this point */,
                  0 /* ellipsisEnd, 0 since text has not been ellipsized at this point */);
          fm.width = (int) Math.ceil(line.metrics(fm));
          TextLine.recycle(line);
  
          return fm;
      }
  
      @Override
      public int getHeight() {
          return mBottom;
      }
  
      @Override
      public int getLineCount() {
          return 1;
      }
  
      @Override
      public int getLineTop(int line) {
          if (line == 0)
              return 0;
          else
              return mBottom;
      }
  
      @Override
      public int getLineDescent(int line) {
          return mDesc;
      }
  
      @Override
      public int getLineStart(int line) {
          if (line == 0)
              return 0;
          else
              return getText().length();
      }
  
      @Override
      public int getParagraphDirection(int line) {
          return DIR_LEFT_TO_RIGHT;
      }
  
      @Override
      public boolean getLineContainsTab(int line) {
          return false;
      }
  
      @Override
      public float getLineMax(int line) {
          return mMax;
      }
  
      @Override
      public float getLineWidth(int line) {
          return (line == 0 ? mMax : 0);
      }
  
      @Override
      public final Directions getLineDirections(int line) {
          return Layout.DIRS_ALL_LEFT_TO_RIGHT;
      }
  
      @Override
      public int getTopPadding() {
          return mTopPadding;
      }
  
      @Override
      public int getBottomPadding() {
          return mBottomPadding;
      }
  
      @Override
      public int getEllipsisCount(int line) {
          return mEllipsizedCount;
      }
  
      @Override
      public int getEllipsisStart(int line) {
          return mEllipsizedStart;
      }
  
      @Override
      public int getEllipsizedWidth() {
          return mEllipsizedWidth;
      }
  
      // Override draw so it will be faster.
      @Override
      public void draw(Canvas c, Path highlight, Paint highlightpaint,
                       int cursorOffset) {
          if (mDirect != null && highlight == null) {
              c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
          } else {
              super.draw(c, highlight, highlightpaint, cursorOffset);
          }
      }
  
      /**
       * Callback for the ellipsizer to report what region it ellipsized.
       */
      public void ellipsized(int start, int end) {
          mEllipsizedStart = start;
          mEllipsizedCount = end - start;
      }
  
      private String mDirect;
      private Paint mPaint;
  
      /* package */ int mBottom, mDesc;   // for Direct
      private int mTopPadding, mBottomPadding;
      private float mMax;
      private int mEllipsizedWidth, mEllipsizedStart, mEllipsizedCount;
  
      public static class Metrics extends Paint.FontMetricsInt {
          public int width;
  
          @Override public String toString() {
              return super.toString() + " width=" + width;
          }
  
          private void reset() {
              top = 0;
              bottom = 0;
              ascent = 0;
              descent = 0;
              width = 0;
              leading = 0;
          }
      }
  }
  