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     public static BoringLayout make(CharSequence source,
     39                         TextPaint paint, int outerwidth,
     40                         Alignment align,
     41                         float spacingmult, float spacingadd,
     42                         BoringLayout.Metrics metrics, boolean includepad) {
     43         return new BoringLayout(source, paint, outerwidth, align,
     44                                 spacingmult, spacingadd, metrics,
     45                                 includepad);
     46     }
     47 
     48     public static BoringLayout make(CharSequence source,
     49                         TextPaint paint, int outerwidth,
     50                         Alignment align,
     51                         float spacingmult, float spacingadd,
     52                         BoringLayout.Metrics metrics, boolean includepad,
     53                         TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
     54         return new BoringLayout(source, paint, outerwidth, align,
     55                                 spacingmult, spacingadd, metrics,
     56                                 includepad, ellipsize, ellipsizedWidth);
     57     }
     58 
     59     /**
     60      * Returns a BoringLayout for the specified text, potentially reusing
     61      * this one if it is already suitable.  The caller must make sure that
     62      * no one is still using this Layout.
     63      */
     64     public BoringLayout replaceOrMake(CharSequence source, TextPaint paint,
     65                                       int outerwidth, Alignment align,
     66                                       float spacingmult, float spacingadd,
     67                                       BoringLayout.Metrics metrics,
     68                                       boolean includepad) {
     69         replaceWith(source, paint, outerwidth, align, spacingmult,
     70                     spacingadd);
     71 
     72         mEllipsizedWidth = outerwidth;
     73         mEllipsizedStart = 0;
     74         mEllipsizedCount = 0;
     75 
     76         init(source, paint, outerwidth, align, spacingmult, spacingadd,
     77              metrics, includepad, true);
     78         return this;
     79     }
     80 
     81     /**
     82      * Returns a BoringLayout for the specified text, potentially reusing
     83      * this one if it is already suitable.  The caller must make sure that
     84      * no one is still using this Layout.
     85      */
     86     public BoringLayout replaceOrMake(CharSequence source, TextPaint paint,
     87                                       int outerwidth, Alignment align,
     88                                       float spacingmult, float spacingadd,
     89                                       BoringLayout.Metrics metrics,
     90                                       boolean includepad,
     91                                       TextUtils.TruncateAt ellipsize,
     92                                       int ellipsizedWidth) {
     93         boolean trust;
     94 
     95         if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {
     96             replaceWith(source, paint, outerwidth, align, spacingmult,
     97                         spacingadd);
     98 
     99             mEllipsizedWidth = outerwidth;
    100             mEllipsizedStart = 0;
    101             mEllipsizedCount = 0;
    102             trust = true;
    103         } else {
    104             replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth,
    105                                            ellipsize, true, this),
    106                         paint, outerwidth, align, spacingmult,
    107                         spacingadd);
    108 
    109             mEllipsizedWidth = ellipsizedWidth;
    110             trust = false;
    111         }
    112 
    113         init(getText(), paint, outerwidth, align, spacingmult, spacingadd,
    114              metrics, includepad, trust);
    115         return this;
    116     }
    117 
    118     public BoringLayout(CharSequence source,
    119                         TextPaint paint, int outerwidth,
    120                         Alignment align,
    121                         float spacingmult, float spacingadd,
    122                         BoringLayout.Metrics metrics, boolean includepad) {
    123         super(source, paint, outerwidth, align, spacingmult, spacingadd);
    124 
    125         mEllipsizedWidth = outerwidth;
    126         mEllipsizedStart = 0;
    127         mEllipsizedCount = 0;
    128 
    129         init(source, paint, outerwidth, align, spacingmult, spacingadd,
    130              metrics, includepad, true);
    131     }
    132 
    133     public BoringLayout(CharSequence source,
    134                         TextPaint paint, int outerwidth,
    135                         Alignment align,
    136                         float spacingmult, float spacingadd,
    137                         BoringLayout.Metrics metrics, boolean includepad,
    138                         TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
    139         /*
    140          * It is silly to have to call super() and then replaceWith(),
    141          * but we can't use "this" for the callback until the call to
    142          * super() finishes.
    143          */
    144         super(source, paint, outerwidth, align, spacingmult, spacingadd);
    145 
    146         boolean trust;
    147 
    148         if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {
    149             mEllipsizedWidth = outerwidth;
    150             mEllipsizedStart = 0;
    151             mEllipsizedCount = 0;
    152             trust = true;
    153         } else {
    154             replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth,
    155                                            ellipsize, true, this),
    156                         paint, outerwidth, align, spacingmult,
    157                         spacingadd);
    158 
    159 
    160             mEllipsizedWidth = ellipsizedWidth;
    161             trust = false;
    162         }
    163 
    164         init(getText(), paint, outerwidth, align, spacingmult, spacingadd,
    165              metrics, includepad, trust);
    166     }
    167 
    168     /* package */ void init(CharSequence source,
    169                             TextPaint paint, int outerwidth,
    170                             Alignment align,
    171                             float spacingmult, float spacingadd,
    172                             BoringLayout.Metrics metrics, boolean includepad,
    173                             boolean trustWidth) {
    174         int spacing;
    175 
    176         if (source instanceof String && align == Layout.Alignment.ALIGN_NORMAL) {
    177             mDirect = source.toString();
    178         } else {
    179             mDirect = null;
    180         }
    181 
    182         mPaint = paint;
    183 
    184         if (includepad) {
    185             spacing = metrics.bottom - metrics.top;
    186             mDesc = metrics.bottom;
    187         } else {
    188             spacing = metrics.descent - metrics.ascent;
    189             mDesc = metrics.descent;
    190         }
    191 
    192         mBottom = spacing;
    193 
    194         if (trustWidth) {
    195             mMax = metrics.width;
    196         } else {
    197             /*
    198              * If we have ellipsized, we have to actually calculate the
    199              * width because the width that was passed in was for the
    200              * full text, not the ellipsized form.
    201              */
    202             TextLine line = TextLine.obtain();
    203             line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT,
    204                     Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
    205             mMax = (int) Math.ceil(line.metrics(null));
    206             TextLine.recycle(line);
    207         }
    208 
    209         if (includepad) {
    210             mTopPadding = metrics.top - metrics.ascent;
    211             mBottomPadding = metrics.bottom - metrics.descent;
    212         }
    213     }
    214 
    215     /**
    216      * Returns null if not boring; the width, ascent, and descent if boring.
    217      */
    218     public static Metrics isBoring(CharSequence text,
    219                                    TextPaint paint) {
    220         return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, null);
    221     }
    222 
    223     /**
    224      * Returns null if not boring; the width, ascent, and descent if boring.
    225      * @hide
    226      */
    227     public static Metrics isBoring(CharSequence text,
    228                                    TextPaint paint,
    229                                    TextDirectionHeuristic textDir) {
    230         return isBoring(text, paint, textDir, null);
    231     }
    232 
    233     /**
    234      * Returns null if not boring; the width, ascent, and descent in the
    235      * provided Metrics object (or a new one if the provided one was null)
    236      * if boring.
    237      */
    238     public static Metrics isBoring(CharSequence text, TextPaint paint, Metrics metrics) {
    239         return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, metrics);
    240     }
    241 
    242     /**
    243      * Returns null if not boring; the width, ascent, and descent in the
    244      * provided Metrics object (or a new one if the provided one was null)
    245      * if boring.
    246      * @hide
    247      */
    248     public static Metrics isBoring(CharSequence text, TextPaint paint,
    249             TextDirectionHeuristic textDir, Metrics metrics) {
    250         final int MAX_BUF_LEN = 500;
    251         final char[] buffer = TextUtils.obtain(MAX_BUF_LEN);
    252         final int textLength = text.length();
    253         boolean boring = true;
    254 
    255         outer:
    256         for (int start = 0; start < textLength; start += MAX_BUF_LEN) {
    257             final int end = Math.min(start + MAX_BUF_LEN, textLength);
    258 
    259             // No need to worry about getting half codepoints, since we reject surrogate code units
    260             // as non-boring as soon we see one.
    261             TextUtils.getChars(text, start, end, buffer, 0);
    262 
    263             final int len = end - start;
    264             for (int i = 0; i < len; i++) {
    265                 final char c = buffer[i];
    266 
    267                 if (c == '\n' || c == '\t' ||
    268                         (c >= 0x0590 && c <= 0x08FF) ||  // RTL scripts
    269                         c == 0x200F ||  // Bidi format character
    270                         (c >= 0x202A && c <= 0x202E) ||  // Bidi format characters
    271                         (c >= 0x2066 && c <= 0x2069) ||  // Bidi format characters
    272                         (c >= 0xD800 && c <= 0xDFFF) ||  // surrogate pairs
    273                         (c >= 0xFB1D && c <= 0xFDFF) ||  // Hebrew and Arabic presentation forms
    274                         (c >= 0xFE70 && c <= 0xFEFE) // Arabic presentation forms
    275                    ) {
    276                     boring = false;
    277                     break outer;
    278                 }
    279             }
    280 
    281             // TODO: This looks a little suspicious, and in some cases can result in O(n^2)
    282             // run time. Consider moving outside the loop.
    283             if (textDir != null && textDir.isRtl(buffer, 0, len)) {
    284                boring = false;
    285                break outer;
    286             }
    287         }
    288 
    289         TextUtils.recycle(buffer);
    290 
    291         if (boring && text instanceof Spanned) {
    292             Spanned sp = (Spanned) text;
    293             Object[] styles = sp.getSpans(0, textLength, ParagraphStyle.class);
    294             if (styles.length > 0) {
    295                 boring = false;
    296             }
    297         }
    298 
    299         if (boring) {
    300             Metrics fm = metrics;
    301             if (fm == null) {
    302                 fm = new Metrics();
    303             } else {
    304                 fm.reset();
    305             }
    306 
    307             TextLine line = TextLine.obtain();
    308             line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT,
    309                     Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
    310             fm.width = (int) Math.ceil(line.metrics(fm));
    311             TextLine.recycle(line);
    312 
    313             return fm;
    314         } else {
    315             return null;
    316         }
    317     }
    318 
    319     @Override
    320     public int getHeight() {
    321         return mBottom;
    322     }
    323 
    324     @Override
    325     public int getLineCount() {
    326         return 1;
    327     }
    328 
    329     @Override
    330     public int getLineTop(int line) {
    331         if (line == 0)
    332             return 0;
    333         else
    334             return mBottom;
    335     }
    336 
    337     @Override
    338     public int getLineDescent(int line) {
    339         return mDesc;
    340     }
    341 
    342     @Override
    343     public int getLineStart(int line) {
    344         if (line == 0)
    345             return 0;
    346         else
    347             return getText().length();
    348     }
    349 
    350     @Override
    351     public int getParagraphDirection(int line) {
    352         return DIR_LEFT_TO_RIGHT;
    353     }
    354 
    355     @Override
    356     public boolean getLineContainsTab(int line) {
    357         return false;
    358     }
    359 
    360     @Override
    361     public float getLineMax(int line) {
    362         return mMax;
    363     }
    364 
    365     @Override
    366     public float getLineWidth(int line) {
    367         return (line == 0 ? mMax : 0);
    368     }
    369 
    370     @Override
    371     public final Directions getLineDirections(int line) {
    372         return Layout.DIRS_ALL_LEFT_TO_RIGHT;
    373     }
    374 
    375     @Override
    376     public int getTopPadding() {
    377         return mTopPadding;
    378     }
    379 
    380     @Override
    381     public int getBottomPadding() {
    382         return mBottomPadding;
    383     }
    384 
    385     @Override
    386     public int getEllipsisCount(int line) {
    387         return mEllipsizedCount;
    388     }
    389 
    390     @Override
    391     public int getEllipsisStart(int line) {
    392         return mEllipsizedStart;
    393     }
    394 
    395     @Override
    396     public int getEllipsizedWidth() {
    397         return mEllipsizedWidth;
    398     }
    399 
    400     // Override draw so it will be faster.
    401     @Override
    402     public void draw(Canvas c, Path highlight, Paint highlightpaint,
    403                      int cursorOffset) {
    404         if (mDirect != null && highlight == null) {
    405             c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
    406         } else {
    407             super.draw(c, highlight, highlightpaint, cursorOffset);
    408         }
    409     }
    410 
    411     /**
    412      * Callback for the ellipsizer to report what region it ellipsized.
    413      */
    414     public void ellipsized(int start, int end) {
    415         mEllipsizedStart = start;
    416         mEllipsizedCount = end - start;
    417     }
    418 
    419     private String mDirect;
    420     private Paint mPaint;
    421 
    422     /* package */ int mBottom, mDesc;   // for Direct
    423     private int mTopPadding, mBottomPadding;
    424     private float mMax;
    425     private int mEllipsizedWidth, mEllipsizedStart, mEllipsizedCount;
    426 
    427     public static class Metrics extends Paint.FontMetricsInt {
    428         public int width;
    429 
    430         @Override public String toString() {
    431             return super.toString() + " width=" + width;
    432         }
    433 
    434         private void reset() {
    435             top = 0;
    436             bottom = 0;
    437             ascent = 0;
    438             descent = 0;
    439             width = 0;
    440             leading = 0;
    441         }
    442     }
    443 }
    444