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.graphics.Canvas;
     20 import android.graphics.Paint;
     21 import android.graphics.Path;
     22 import android.text.style.ParagraphStyle;
     23 
     24 /**
     25  * A BoringLayout is a very simple Layout implementation for text that
     26  * fits on a single line and is all left-to-right characters.
     27  * You will probably never want to make one of these yourself;
     28  * if you do, be sure to call {@link #isBoring} first to make sure
     29  * the text meets the criteria.
     30  * <p>This class is used by widgets to control text layout. You should not need
     31  * to use this class directly unless you are implementing your own widget
     32  * or custom display object, in which case
     33  * you are encouraged to use a Layout instead of calling
     34  * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
     35  *  Canvas.drawText()} directly.</p>
     36  */
     37 public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback {
     38 
     39     /**
     40      * Utility function to construct a BoringLayout instance.
     41      *
     42      * @param source the text to render
     43      * @param paint the default paint for the layout
     44      * @param outerWidth the wrapping width for the text
     45      * @param align whether to left, right, or center the text
     46      * @param spacingMult this value is no longer used by BoringLayout
     47      * @param spacingAdd this value is no longer used by BoringLayout
     48      * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
     49      *                line width
     50      * @param includePad set whether to include extra space beyond font ascent and descent which is
     51      *                   needed to avoid clipping in some scripts
     52      */
     53     public static BoringLayout make(CharSequence source, TextPaint paint, int outerWidth,
     54             Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics,
     55             boolean includePad) {
     56         return new BoringLayout(source, paint, outerWidth, align, spacingMult, spacingAdd, metrics,
     57                 includePad);
     58     }
     59 
     60     /**
     61      * Utility function to construct a BoringLayout instance.
     62      *
     63      * @param source the text to render
     64      * @param paint the default paint for the layout
     65      * @param outerWidth the wrapping width for the text
     66      * @param align whether to left, right, or center the text
     67      * @param spacingmult this value is no longer used by BoringLayout
     68      * @param spacingadd this value is no longer used by BoringLayout
     69      * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
     70      *                line width
     71      * @param includePad set whether to include extra space beyond font ascent and descent which is
     72      *                   needed to avoid clipping in some scripts
     73      * @param ellipsize whether to ellipsize the text if width of the text is longer than the
     74      *                  requested width
     75      * @param ellipsizedWidth the width to which this Layout is ellipsizing. If {@code ellipsize} is
     76      *                        {@code null}, or is {@link TextUtils.TruncateAt#MARQUEE} this value is
     77      *                        not used, {@code outerWidth} is used instead
     78      */
     79     public static BoringLayout make(CharSequence source, TextPaint paint, int outerWidth,
     80             Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics,
     81             boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
     82         return new BoringLayout(source, paint, outerWidth, align, spacingmult, spacingadd, metrics,
     83                 includePad, ellipsize, ellipsizedWidth);
     84     }
     85 
     86     /**
     87      * Returns a BoringLayout for the specified text, potentially reusing
     88      * this one if it is already suitable.  The caller must make sure that
     89      * no one is still using this Layout.
     90      *
     91      * @param source the text to render
     92      * @param paint the default paint for the layout
     93      * @param outerwidth the wrapping width for the text
     94      * @param align whether to left, right, or center the text
     95      * @param spacingMult this value is no longer used by BoringLayout
     96      * @param spacingAdd this value is no longer used by BoringLayout
     97      * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
     98      *                line width
     99      * @param includePad set whether to include extra space beyond font ascent and descent which is
    100      *                   needed to avoid clipping in some scripts
    101      */
    102     public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, int outerwidth,
    103             Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics,
    104             boolean includePad) {
    105         replaceWith(source, paint, outerwidth, align, spacingMult, spacingAdd);
    106 
    107         mEllipsizedWidth = outerwidth;
    108         mEllipsizedStart = 0;
    109         mEllipsizedCount = 0;
    110 
    111         init(source, paint, align, metrics, includePad, true);
    112         return this;
    113     }
    114 
    115     /**
    116      * Returns a BoringLayout for the specified text, potentially reusing
    117      * this one if it is already suitable.  The caller must make sure that
    118      * no one is still using this Layout.
    119      *
    120      * @param source the text to render
    121      * @param paint the default paint for the layout
    122      * @param outerWidth the wrapping width for the text
    123      * @param align whether to left, right, or center the text
    124      * @param spacingMult this value is no longer used by BoringLayout
    125      * @param spacingAdd this value is no longer used by BoringLayout
    126      * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
    127      *                line width
    128      * @param includePad set whether to include extra space beyond font ascent and descent which is
    129      *                   needed to avoid clipping in some scripts
    130      * @param ellipsize whether to ellipsize the text if width of the text is longer than the
    131      *                  requested width
    132      * @param ellipsizedWidth the width to which this Layout is ellipsizing. If {@code ellipsize} is
    133      *                        {@code null}, or is {@link TextUtils.TruncateAt#MARQUEE} this value is
    134      *                        not used, {@code outerwidth} is used instead
    135      */
    136     public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, int outerWidth,
    137             Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics,
    138             boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
    139         boolean trust;
    140 
    141         if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {
    142             replaceWith(source, paint, outerWidth, align, spacingMult, spacingAdd);
    143 
    144             mEllipsizedWidth = outerWidth;
    145             mEllipsizedStart = 0;
    146             mEllipsizedCount = 0;
    147             trust = true;
    148         } else {
    149             replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true, this),
    150                     paint, outerWidth, align, spacingMult, spacingAdd);
    151 
    152             mEllipsizedWidth = ellipsizedWidth;
    153             trust = false;
    154         }
    155 
    156         init(getText(), paint, align, metrics, includePad, trust);
    157         return this;
    158     }
    159 
    160     /**
    161      * @param source the text to render
    162      * @param paint the default paint for the layout
    163      * @param outerwidth the wrapping width for the text
    164      * @param align whether to left, right, or center the text
    165      * @param spacingMult this value is no longer used by BoringLayout
    166      * @param spacingAdd this value is no longer used by BoringLayout
    167      * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
    168      *                line width
    169      * @param includePad set whether to include extra space beyond font ascent and descent which is
    170      *                   needed to avoid clipping in some scripts
    171      */
    172     public BoringLayout(CharSequence source, TextPaint paint, int outerwidth, Alignment align,
    173             float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad) {
    174         super(source, paint, outerwidth, align, spacingMult, spacingAdd);
    175 
    176         mEllipsizedWidth = outerwidth;
    177         mEllipsizedStart = 0;
    178         mEllipsizedCount = 0;
    179 
    180         init(source, paint, align, metrics, includePad, true);
    181     }
    182 
    183     /**
    184      *
    185      * @param source the text to render
    186      * @param paint the default paint for the layout
    187      * @param outerWidth the wrapping width for the text
    188      * @param align whether to left, right, or center the text
    189      * @param spacingMult this value is no longer used by BoringLayout
    190      * @param spacingAdd this value is no longer used by BoringLayout
    191      * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
    192      *                line width
    193      * @param includePad set whether to include extra space beyond font ascent and descent which is
    194      *                   needed to avoid clipping in some scripts
    195      * @param ellipsize whether to ellipsize the text if width of the text is longer than the
    196      *                  requested {@code outerwidth}
    197      * @param ellipsizedWidth the width to which this Layout is ellipsizing. If {@code ellipsize} is
    198      *                        {@code null}, or is {@link TextUtils.TruncateAt#MARQUEE} this value is
    199      *                        not used, {@code outerwidth} is used instead
    200      */
    201     public BoringLayout(CharSequence source, TextPaint paint, int outerWidth, Alignment align,
    202             float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad,
    203             TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
    204         /*
    205          * It is silly to have to call super() and then replaceWith(),
    206          * but we can't use "this" for the callback until the call to
    207          * super() finishes.
    208          */
    209         super(source, paint, outerWidth, align, spacingMult, spacingAdd);
    210 
    211         boolean trust;
    212 
    213         if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {
    214             mEllipsizedWidth = outerWidth;
    215             mEllipsizedStart = 0;
    216             mEllipsizedCount = 0;
    217             trust = true;
    218         } else {
    219             replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true, this),
    220                         paint, outerWidth, align, spacingMult, spacingAdd);
    221 
    222             mEllipsizedWidth = ellipsizedWidth;
    223             trust = false;
    224         }
    225 
    226         init(getText(), paint, align, metrics, includePad, trust);
    227     }
    228 
    229     /* package */ void init(CharSequence source, TextPaint paint, Alignment align,
    230             BoringLayout.Metrics metrics, boolean includePad, boolean trustWidth) {
    231         int spacing;
    232 
    233         if (source instanceof String && align == Layout.Alignment.ALIGN_NORMAL) {
    234             mDirect = source.toString();
    235         } else {
    236             mDirect = null;
    237         }
    238 
    239         mPaint = paint;
    240 
    241         if (includePad) {
    242             spacing = metrics.bottom - metrics.top;
    243             mDesc = metrics.bottom;
    244         } else {
    245             spacing = metrics.descent - metrics.ascent;
    246             mDesc = metrics.descent;
    247         }
    248 
    249         mBottom = spacing;
    250 
    251         if (trustWidth) {
    252             mMax = metrics.width;
    253         } else {
    254             /*
    255              * If we have ellipsized, we have to actually calculate the
    256              * width because the width that was passed in was for the
    257              * full text, not the ellipsized form.
    258              */
    259             TextLine line = TextLine.obtain();
    260             line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT,
    261                     Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
    262             mMax = (int) Math.ceil(line.metrics(null));
    263             TextLine.recycle(line);
    264         }
    265 
    266         if (includePad) {
    267             mTopPadding = metrics.top - metrics.ascent;
    268             mBottomPadding = metrics.bottom - metrics.descent;
    269         }
    270     }
    271 
    272     /**
    273      * Returns null if not boring; the width, ascent, and descent if boring.
    274      */
    275     public static Metrics isBoring(CharSequence text, TextPaint paint) {
    276         return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, null);
    277     }
    278 
    279     /**
    280      * Returns null if not boring; the width, ascent, and descent in the
    281      * provided Metrics object (or a new one if the provided one was null)
    282      * if boring.
    283      */
    284     public static Metrics isBoring(CharSequence text, TextPaint paint, Metrics metrics) {
    285         return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, metrics);
    286     }
    287 
    288     /**
    289      * Returns true if the text contains any RTL characters, bidi format characters, or surrogate
    290      * code units.
    291      */
    292     private static boolean hasAnyInterestingChars(CharSequence text, int textLength) {
    293         final int MAX_BUF_LEN = 500;
    294         final char[] buffer = TextUtils.obtain(MAX_BUF_LEN);
    295         try {
    296             for (int start = 0; start < textLength; start += MAX_BUF_LEN) {
    297                 final int end = Math.min(start + MAX_BUF_LEN, textLength);
    298 
    299                 // No need to worry about getting half codepoints, since we consider surrogate code
    300                 // units "interesting" as soon we see one.
    301                 TextUtils.getChars(text, start, end, buffer, 0);
    302 
    303                 final int len = end - start;
    304                 for (int i = 0; i < len; i++) {
    305                     final char c = buffer[i];
    306                     if (c == '\n' || c == '\t' || TextUtils.couldAffectRtl(c)) {
    307                         return true;
    308                     }
    309                 }
    310             }
    311             return false;
    312         } finally {
    313             TextUtils.recycle(buffer);
    314         }
    315     }
    316 
    317     /**
    318      * Returns null if not boring; the width, ascent, and descent in the
    319      * provided Metrics object (or a new one if the provided one was null)
    320      * if boring.
    321      * @hide
    322      */
    323     public static Metrics isBoring(CharSequence text, TextPaint paint,
    324             TextDirectionHeuristic textDir, Metrics metrics) {
    325         final int textLength = text.length();
    326         if (hasAnyInterestingChars(text, textLength)) {
    327            return null;  // There are some interesting characters. Not boring.
    328         }
    329         if (textDir != null && textDir.isRtl(text, 0, textLength)) {
    330            return null;  // The heuristic considers the whole text RTL. Not boring.
    331         }
    332         if (text instanceof Spanned) {
    333             Spanned sp = (Spanned) text;
    334             Object[] styles = sp.getSpans(0, textLength, ParagraphStyle.class);
    335             if (styles.length > 0) {
    336                 return null;  // There are some PargraphStyle spans. Not boring.
    337             }
    338         }
    339 
    340         Metrics fm = metrics;
    341         if (fm == null) {
    342             fm = new Metrics();
    343         } else {
    344             fm.reset();
    345         }
    346 
    347         TextLine line = TextLine.obtain();
    348         line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT,
    349                 Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
    350         fm.width = (int) Math.ceil(line.metrics(fm));
    351         TextLine.recycle(line);
    352 
    353         return fm;
    354     }
    355 
    356     @Override
    357     public int getHeight() {
    358         return mBottom;
    359     }
    360 
    361     @Override
    362     public int getLineCount() {
    363         return 1;
    364     }
    365 
    366     @Override
    367     public int getLineTop(int line) {
    368         if (line == 0)
    369             return 0;
    370         else
    371             return mBottom;
    372     }
    373 
    374     @Override
    375     public int getLineDescent(int line) {
    376         return mDesc;
    377     }
    378 
    379     @Override
    380     public int getLineStart(int line) {
    381         if (line == 0)
    382             return 0;
    383         else
    384             return getText().length();
    385     }
    386 
    387     @Override
    388     public int getParagraphDirection(int line) {
    389         return DIR_LEFT_TO_RIGHT;
    390     }
    391 
    392     @Override
    393     public boolean getLineContainsTab(int line) {
    394         return false;
    395     }
    396 
    397     @Override
    398     public float getLineMax(int line) {
    399         return mMax;
    400     }
    401 
    402     @Override
    403     public float getLineWidth(int line) {
    404         return (line == 0 ? mMax : 0);
    405     }
    406 
    407     @Override
    408     public final Directions getLineDirections(int line) {
    409         return Layout.DIRS_ALL_LEFT_TO_RIGHT;
    410     }
    411 
    412     @Override
    413     public int getTopPadding() {
    414         return mTopPadding;
    415     }
    416 
    417     @Override
    418     public int getBottomPadding() {
    419         return mBottomPadding;
    420     }
    421 
    422     @Override
    423     public int getEllipsisCount(int line) {
    424         return mEllipsizedCount;
    425     }
    426 
    427     @Override
    428     public int getEllipsisStart(int line) {
    429         return mEllipsizedStart;
    430     }
    431 
    432     @Override
    433     public int getEllipsizedWidth() {
    434         return mEllipsizedWidth;
    435     }
    436 
    437     // Override draw so it will be faster.
    438     @Override
    439     public void draw(Canvas c, Path highlight, Paint highlightpaint,
    440                      int cursorOffset) {
    441         if (mDirect != null && highlight == null) {
    442             c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
    443         } else {
    444             super.draw(c, highlight, highlightpaint, cursorOffset);
    445         }
    446     }
    447 
    448     /**
    449      * Callback for the ellipsizer to report what region it ellipsized.
    450      */
    451     public void ellipsized(int start, int end) {
    452         mEllipsizedStart = start;
    453         mEllipsizedCount = end - start;
    454     }
    455 
    456     private String mDirect;
    457     private Paint mPaint;
    458 
    459     /* package */ int mBottom, mDesc;   // for Direct
    460     private int mTopPadding, mBottomPadding;
    461     private float mMax;
    462     private int mEllipsizedWidth, mEllipsizedStart, mEllipsizedCount;
    463 
    464     public static class Metrics extends Paint.FontMetricsInt {
    465         public int width;
    466 
    467         @Override public String toString() {
    468             return super.toString() + " width=" + width;
    469         }
    470 
    471         private void reset() {
    472             top = 0;
    473             bottom = 0;
    474             ascent = 0;
    475             descent = 0;
    476             width = 0;
    477             leading = 0;
    478         }
    479     }
    480 }
    481