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.Bitmap;
     20 import android.graphics.Paint;
     21 import android.text.style.LeadingMarginSpan;
     22 import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
     23 import android.text.style.LineHeightSpan;
     24 import android.text.style.MetricAffectingSpan;
     25 import android.text.style.TabStopSpan;
     26 import android.util.Log;
     27 
     28 import com.android.internal.util.ArrayUtils;
     29 
     30 /**
     31  * StaticLayout is a Layout for text that will not be edited after it
     32  * is laid out.  Use {@link DynamicLayout} for text that may change.
     33  * <p>This is used by widgets to control text layout. You should not need
     34  * to use this class directly unless you are implementing your own widget
     35  * or custom display object, or would be tempted to call
     36  * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int,
     37  * float, float, android.graphics.Paint)
     38  * Canvas.drawText()} directly.</p>
     39  */
     40 public class StaticLayout extends Layout {
     41 
     42     static final String TAG = "StaticLayout";
     43 
     44     public StaticLayout(CharSequence source, TextPaint paint,
     45                         int width,
     46                         Alignment align, float spacingmult, float spacingadd,
     47                         boolean includepad) {
     48         this(source, 0, source.length(), paint, width, align,
     49              spacingmult, spacingadd, includepad);
     50     }
     51 
     52     /**
     53      * @hide
     54      */
     55     public StaticLayout(CharSequence source, TextPaint paint,
     56             int width, Alignment align, TextDirectionHeuristic textDir,
     57             float spacingmult, float spacingadd,
     58             boolean includepad) {
     59         this(source, 0, source.length(), paint, width, align, textDir,
     60                 spacingmult, spacingadd, includepad);
     61     }
     62 
     63     public StaticLayout(CharSequence source, int bufstart, int bufend,
     64                         TextPaint paint, int outerwidth,
     65                         Alignment align,
     66                         float spacingmult, float spacingadd,
     67                         boolean includepad) {
     68         this(source, bufstart, bufend, paint, outerwidth, align,
     69              spacingmult, spacingadd, includepad, null, 0);
     70     }
     71 
     72     /**
     73      * @hide
     74      */
     75     public StaticLayout(CharSequence source, int bufstart, int bufend,
     76             TextPaint paint, int outerwidth,
     77             Alignment align, TextDirectionHeuristic textDir,
     78             float spacingmult, float spacingadd,
     79             boolean includepad) {
     80         this(source, bufstart, bufend, paint, outerwidth, align, textDir,
     81                 spacingmult, spacingadd, includepad, null, 0, Integer.MAX_VALUE);
     82 }
     83 
     84     public StaticLayout(CharSequence source, int bufstart, int bufend,
     85             TextPaint paint, int outerwidth,
     86             Alignment align,
     87             float spacingmult, float spacingadd,
     88             boolean includepad,
     89             TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
     90         this(source, bufstart, bufend, paint, outerwidth, align,
     91                 TextDirectionHeuristics.FIRSTSTRONG_LTR,
     92                 spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
     93     }
     94 
     95     /**
     96      * @hide
     97      */
     98     public StaticLayout(CharSequence source, int bufstart, int bufend,
     99                         TextPaint paint, int outerwidth,
    100                         Alignment align, TextDirectionHeuristic textDir,
    101                         float spacingmult, float spacingadd,
    102                         boolean includepad,
    103                         TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) {
    104         super((ellipsize == null)
    105                 ? source
    106                 : (source instanceof Spanned)
    107                     ? new SpannedEllipsizer(source)
    108                     : new Ellipsizer(source),
    109               paint, outerwidth, align, textDir, spacingmult, spacingadd);
    110 
    111         /*
    112          * This is annoying, but we can't refer to the layout until
    113          * superclass construction is finished, and the superclass
    114          * constructor wants the reference to the display text.
    115          *
    116          * This will break if the superclass constructor ever actually
    117          * cares about the content instead of just holding the reference.
    118          */
    119         if (ellipsize != null) {
    120             Ellipsizer e = (Ellipsizer) getText();
    121 
    122             e.mLayout = this;
    123             e.mWidth = ellipsizedWidth;
    124             e.mMethod = ellipsize;
    125             mEllipsizedWidth = ellipsizedWidth;
    126 
    127             mColumns = COLUMNS_ELLIPSIZE;
    128         } else {
    129             mColumns = COLUMNS_NORMAL;
    130             mEllipsizedWidth = outerwidth;
    131         }
    132 
    133         mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)];
    134         mLineDirections = new Directions[
    135                              ArrayUtils.idealIntArraySize(2 * mColumns)];
    136         mMaximumVisibleLineCount = maxLines;
    137 
    138         mMeasured = MeasuredText.obtain();
    139 
    140         generate(source, bufstart, bufend, paint, outerwidth, textDir, spacingmult,
    141                  spacingadd, includepad, includepad, ellipsizedWidth,
    142                  ellipsize);
    143 
    144         mMeasured = MeasuredText.recycle(mMeasured);
    145         mFontMetricsInt = null;
    146     }
    147 
    148     /* package */ StaticLayout(CharSequence text) {
    149         super(text, null, 0, null, 0, 0);
    150 
    151         mColumns = COLUMNS_ELLIPSIZE;
    152         mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)];
    153         mLineDirections = new Directions[
    154                              ArrayUtils.idealIntArraySize(2 * mColumns)];
    155         mMeasured = MeasuredText.obtain();
    156     }
    157 
    158     /* package */ void generate(CharSequence source, int bufStart, int bufEnd,
    159                         TextPaint paint, int outerWidth,
    160                         TextDirectionHeuristic textDir, float spacingmult,
    161                         float spacingadd, boolean includepad,
    162                         boolean trackpad, float ellipsizedWidth,
    163                         TextUtils.TruncateAt ellipsize) {
    164         mLineCount = 0;
    165 
    166         int v = 0;
    167         boolean needMultiply = (spacingmult != 1 || spacingadd != 0);
    168 
    169         Paint.FontMetricsInt fm = mFontMetricsInt;
    170         int[] chooseHtv = null;
    171 
    172         MeasuredText measured = mMeasured;
    173 
    174         Spanned spanned = null;
    175         if (source instanceof Spanned)
    176             spanned = (Spanned) source;
    177 
    178         int DEFAULT_DIR = DIR_LEFT_TO_RIGHT; // XXX
    179 
    180         int paraEnd;
    181         for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
    182             paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd);
    183             if (paraEnd < 0)
    184                 paraEnd = bufEnd;
    185             else
    186                 paraEnd++;
    187 
    188             int firstWidthLineLimit = mLineCount + 1;
    189             int firstWidth = outerWidth;
    190             int restWidth = outerWidth;
    191 
    192             LineHeightSpan[] chooseHt = null;
    193 
    194             if (spanned != null) {
    195                 LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd,
    196                         LeadingMarginSpan.class);
    197                 for (int i = 0; i < sp.length; i++) {
    198                     LeadingMarginSpan lms = sp[i];
    199                     firstWidth -= sp[i].getLeadingMargin(true);
    200                     restWidth -= sp[i].getLeadingMargin(false);
    201 
    202                     // LeadingMarginSpan2 is odd.  The count affects all
    203                     // leading margin spans, not just this particular one,
    204                     // and start from the top of the span, not the top of the
    205                     // paragraph.
    206                     if (lms instanceof LeadingMarginSpan2) {
    207                         LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms;
    208                         int lmsFirstLine = getLineForOffset(spanned.getSpanStart(lms2));
    209                         firstWidthLineLimit = lmsFirstLine + lms2.getLeadingMarginLineCount();
    210                     }
    211                 }
    212 
    213                 chooseHt = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class);
    214 
    215                 if (chooseHt.length != 0) {
    216                     if (chooseHtv == null ||
    217                         chooseHtv.length < chooseHt.length) {
    218                         chooseHtv = new int[ArrayUtils.idealIntArraySize(
    219                                             chooseHt.length)];
    220                     }
    221 
    222                     for (int i = 0; i < chooseHt.length; i++) {
    223                         int o = spanned.getSpanStart(chooseHt[i]);
    224 
    225                         if (o < paraStart) {
    226                             // starts in this layout, before the
    227                             // current paragraph
    228 
    229                             chooseHtv[i] = getLineTop(getLineForOffset(o));
    230                         } else {
    231                             // starts in this paragraph
    232 
    233                             chooseHtv[i] = v;
    234                         }
    235                     }
    236                 }
    237             }
    238 
    239             measured.setPara(source, paraStart, paraEnd, textDir);
    240             char[] chs = measured.mChars;
    241             float[] widths = measured.mWidths;
    242             byte[] chdirs = measured.mLevels;
    243             int dir = measured.mDir;
    244             boolean easy = measured.mEasy;
    245 
    246             int width = firstWidth;
    247 
    248             float w = 0;
    249             int here = paraStart;
    250 
    251             int ok = paraStart;
    252             float okWidth = w;
    253             int okAscent = 0, okDescent = 0, okTop = 0, okBottom = 0;
    254 
    255             int fit = paraStart;
    256             float fitWidth = w;
    257             int fitAscent = 0, fitDescent = 0, fitTop = 0, fitBottom = 0;
    258 
    259             boolean hasTabOrEmoji = false;
    260             boolean hasTab = false;
    261             TabStops tabStops = null;
    262 
    263             for (int spanStart = paraStart, spanEnd = spanStart, nextSpanStart;
    264                     spanStart < paraEnd; spanStart = nextSpanStart) {
    265 
    266                 if (spanStart == spanEnd) {
    267                     if (spanned == null)
    268                         spanEnd = paraEnd;
    269                     else
    270                         spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
    271                                 MetricAffectingSpan.class);
    272 
    273                     int spanLen = spanEnd - spanStart;
    274                     if (spanned == null) {
    275                         measured.addStyleRun(paint, spanLen, fm);
    276                     } else {
    277                         MetricAffectingSpan[] spans =
    278                             spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
    279                         spans = TextUtils.removeEmptySpans(spans, spanned,
    280                                 MetricAffectingSpan.class);
    281                         measured.addStyleRun(paint, spans, spanLen, fm);
    282                     }
    283                 }
    284 
    285                 nextSpanStart = spanEnd;
    286 
    287                 int fmTop = fm.top;
    288                 int fmBottom = fm.bottom;
    289                 int fmAscent = fm.ascent;
    290                 int fmDescent = fm.descent;
    291 
    292                 for (int j = spanStart; j < spanEnd; j++) {
    293                     char c = chs[j - paraStart];
    294 
    295                     if (c == CHAR_NEW_LINE) {
    296                         // intentionally left empty
    297                     } else if (c == CHAR_TAB) {
    298                         if (hasTab == false) {
    299                             hasTab = true;
    300                             hasTabOrEmoji = true;
    301                             if (spanned != null) {
    302                                 // First tab this para, check for tabstops
    303                                 TabStopSpan[] spans = getParagraphSpans(spanned, paraStart,
    304                                         paraEnd, TabStopSpan.class);
    305                                 if (spans.length > 0) {
    306                                     tabStops = new TabStops(TAB_INCREMENT, spans);
    307                                 }
    308                             }
    309                         }
    310                         if (tabStops != null) {
    311                             w = tabStops.nextTab(w);
    312                         } else {
    313                             w = TabStops.nextDefaultStop(w, TAB_INCREMENT);
    314                         }
    315                     } else if (c >= CHAR_FIRST_HIGH_SURROGATE && c <= CHAR_LAST_LOW_SURROGATE
    316                             && j + 1 < spanEnd) {
    317                         int emoji = Character.codePointAt(chs, j - paraStart);
    318 
    319                         if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) {
    320                             Bitmap bm = EMOJI_FACTORY.getBitmapFromAndroidPua(emoji);
    321 
    322                             if (bm != null) {
    323                                 Paint whichPaint;
    324 
    325                                 if (spanned == null) {
    326                                     whichPaint = paint;
    327                                 } else {
    328                                     whichPaint = mWorkPaint;
    329                                 }
    330 
    331                                 float wid = bm.getWidth() * -whichPaint.ascent() / bm.getHeight();
    332 
    333                                 w += wid;
    334                                 hasTabOrEmoji = true;
    335                                 j++;
    336                             } else {
    337                                 w += widths[j - paraStart];
    338                             }
    339                         } else {
    340                             w += widths[j - paraStart];
    341                         }
    342                     } else {
    343                         w += widths[j - paraStart];
    344                     }
    345 
    346                     // Log.e("text", "was " + before + " now " + w + " after " + c + " within " + width);
    347 
    348                     if (w <= width) {
    349                         fitWidth = w;
    350                         fit = j + 1;
    351 
    352                         if (fmTop < fitTop)
    353                             fitTop = fmTop;
    354                         if (fmAscent < fitAscent)
    355                             fitAscent = fmAscent;
    356                         if (fmDescent > fitDescent)
    357                             fitDescent = fmDescent;
    358                         if (fmBottom > fitBottom)
    359                             fitBottom = fmBottom;
    360 
    361                         /*
    362                          * From the Unicode Line Breaking Algorithm:
    363                          * (at least approximately)
    364                          *
    365                          * .,:; are class IS: breakpoints
    366                          *      except when adjacent to digits
    367                          * /    is class SY: a breakpoint
    368                          *      except when followed by a digit.
    369                          * -    is class HY: a breakpoint
    370                          *      except when followed by a digit.
    371                          *
    372                          * Ideographs are class ID: breakpoints when adjacent,
    373                          * except for NS (non-starters), which can be broken
    374                          * after but not before.
    375                          */
    376 
    377                         if (c == CHAR_SPACE || c == CHAR_TAB ||
    378                             ((c == CHAR_DOT || c == CHAR_COMMA ||
    379                                     c == CHAR_COLON || c == CHAR_SEMICOLON) &&
    380                              (j - 1 < here || !Character.isDigit(chs[j - 1 - paraStart])) &&
    381                              (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) ||
    382                             ((c == CHAR_SLASH || c == CHAR_HYPHEN) &&
    383                              (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) ||
    384                             (c >= CHAR_FIRST_CJK && isIdeographic(c, true) &&
    385                              j + 1 < spanEnd && isIdeographic(chs[j + 1 - paraStart], false))) {
    386                             okWidth = w;
    387                             ok = j + 1;
    388 
    389                             if (fitTop < okTop)
    390                                 okTop = fitTop;
    391                             if (fitAscent < okAscent)
    392                                 okAscent = fitAscent;
    393                             if (fitDescent > okDescent)
    394                                 okDescent = fitDescent;
    395                             if (fitBottom > okBottom)
    396                                 okBottom = fitBottom;
    397                         }
    398                     } else {
    399                         final boolean moreChars = (j + 1 < spanEnd);
    400                         int endPos;
    401                         int above, below, top, bottom;
    402                         float currentTextWidth;
    403 
    404                         if (ok != here) {
    405                             // If it is a space that makes the length exceed width, cut here
    406                             if (c == CHAR_SPACE) ok = j + 1;
    407 
    408                             while (ok < spanEnd && chs[ok - paraStart] == CHAR_SPACE) {
    409                                 ok++;
    410                             }
    411 
    412                             endPos = ok;
    413                             above = okAscent;
    414                             below = okDescent;
    415                             top = okTop;
    416                             bottom = okBottom;
    417                             currentTextWidth = okWidth;
    418                         } else if (fit != here) {
    419                             endPos = fit;
    420                             above = fitAscent;
    421                             below = fitDescent;
    422                             top = fitTop;
    423                             bottom = fitBottom;
    424                             currentTextWidth = fitWidth;
    425                         } else {
    426                             endPos = here + 1;
    427                             above = fm.ascent;
    428                             below = fm.descent;
    429                             top = fm.top;
    430                             bottom = fm.bottom;
    431                             currentTextWidth = widths[here - paraStart];
    432                         }
    433 
    434                         v = out(source, here, endPos,
    435                                 above, below, top, bottom,
    436                                 v, spacingmult, spacingadd, chooseHt,chooseHtv, fm, hasTabOrEmoji,
    437                                 needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad,
    438                                 chs, widths, paraStart, ellipsize, ellipsizedWidth,
    439                                 currentTextWidth, paint, moreChars);
    440                         here = endPos;
    441 
    442                         if (here < spanStart) {
    443                             // didn't output all the text for this span
    444                             // we've measured the raw widths, though, so
    445                             // just reset the start point
    446                             j = nextSpanStart = here;
    447                         } else {
    448                             j = here - 1;    // continue looping
    449                         }
    450 
    451                         ok = fit = here;
    452                         w = 0;
    453                         fitAscent = fitDescent = fitTop = fitBottom = 0;
    454                         okAscent = okDescent = okTop = okBottom = 0;
    455 
    456                         if (--firstWidthLineLimit <= 0) {
    457                             width = restWidth;
    458                         }
    459                     }
    460                     if (mLineCount >= mMaximumVisibleLineCount) {
    461                         break;
    462                     }
    463                 }
    464             }
    465 
    466             if (paraEnd != here && mLineCount < mMaximumVisibleLineCount) {
    467                 if ((fitTop | fitBottom | fitDescent | fitAscent) == 0) {
    468                     paint.getFontMetricsInt(fm);
    469 
    470                     fitTop = fm.top;
    471                     fitBottom = fm.bottom;
    472                     fitAscent = fm.ascent;
    473                     fitDescent = fm.descent;
    474                 }
    475 
    476                 // Log.e("text", "output rest " + here + " to " + end);
    477 
    478                 v = out(source,
    479                         here, paraEnd, fitAscent, fitDescent,
    480                         fitTop, fitBottom,
    481                         v,
    482                         spacingmult, spacingadd, chooseHt,
    483                         chooseHtv, fm, hasTabOrEmoji,
    484                         needMultiply, chdirs, dir, easy, bufEnd,
    485                         includepad, trackpad, chs,
    486                         widths, paraStart, ellipsize,
    487                         ellipsizedWidth, w, paint, paraEnd != bufEnd);
    488             }
    489 
    490             paraStart = paraEnd;
    491 
    492             if (paraEnd == bufEnd)
    493                 break;
    494         }
    495 
    496         if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) &&
    497                 mLineCount < mMaximumVisibleLineCount) {
    498             // Log.e("text", "output last " + bufEnd);
    499 
    500             paint.getFontMetricsInt(fm);
    501 
    502             v = out(source,
    503                     bufEnd, bufEnd, fm.ascent, fm.descent,
    504                     fm.top, fm.bottom,
    505                     v,
    506                     spacingmult, spacingadd, null,
    507                     null, fm, false,
    508                     needMultiply, null, DEFAULT_DIR, true, bufEnd,
    509                     includepad, trackpad, null,
    510                     null, bufStart, ellipsize,
    511                     ellipsizedWidth, 0, paint, false);
    512         }
    513     }
    514 
    515     /**
    516      * Returns true if the specified character is one of those specified
    517      * as being Ideographic (class ID) by the Unicode Line Breaking Algorithm
    518      * (http://www.unicode.org/unicode/reports/tr14/), and is therefore OK
    519      * to break between a pair of.
    520      *
    521      * @param includeNonStarters also return true for category NS
    522      *                           (non-starters), which can be broken
    523      *                           after but not before.
    524      */
    525     private static final boolean isIdeographic(char c, boolean includeNonStarters) {
    526         if (c >= '\u2E80' && c <= '\u2FFF') {
    527             return true; // CJK, KANGXI RADICALS, DESCRIPTION SYMBOLS
    528         }
    529         if (c == '\u3000') {
    530             return true; // IDEOGRAPHIC SPACE
    531         }
    532         if (c >= '\u3040' && c <= '\u309F') {
    533             if (!includeNonStarters) {
    534                 switch (c) {
    535                 case '\u3041': //  # HIRAGANA LETTER SMALL A
    536                 case '\u3043': //  # HIRAGANA LETTER SMALL I
    537                 case '\u3045': //  # HIRAGANA LETTER SMALL U
    538                 case '\u3047': //  # HIRAGANA LETTER SMALL E
    539                 case '\u3049': //  # HIRAGANA LETTER SMALL O
    540                 case '\u3063': //  # HIRAGANA LETTER SMALL TU
    541                 case '\u3083': //  # HIRAGANA LETTER SMALL YA
    542                 case '\u3085': //  # HIRAGANA LETTER SMALL YU
    543                 case '\u3087': //  # HIRAGANA LETTER SMALL YO
    544                 case '\u308E': //  # HIRAGANA LETTER SMALL WA
    545                 case '\u3095': //  # HIRAGANA LETTER SMALL KA
    546                 case '\u3096': //  # HIRAGANA LETTER SMALL KE
    547                 case '\u309B': //  # KATAKANA-HIRAGANA VOICED SOUND MARK
    548                 case '\u309C': //  # KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK
    549                 case '\u309D': //  # HIRAGANA ITERATION MARK
    550                 case '\u309E': //  # HIRAGANA VOICED ITERATION MARK
    551                     return false;
    552                 }
    553             }
    554             return true; // Hiragana (except small characters)
    555         }
    556         if (c >= '\u30A0' && c <= '\u30FF') {
    557             if (!includeNonStarters) {
    558                 switch (c) {
    559                 case '\u30A0': //  # KATAKANA-HIRAGANA DOUBLE HYPHEN
    560                 case '\u30A1': //  # KATAKANA LETTER SMALL A
    561                 case '\u30A3': //  # KATAKANA LETTER SMALL I
    562                 case '\u30A5': //  # KATAKANA LETTER SMALL U
    563                 case '\u30A7': //  # KATAKANA LETTER SMALL E
    564                 case '\u30A9': //  # KATAKANA LETTER SMALL O
    565                 case '\u30C3': //  # KATAKANA LETTER SMALL TU
    566                 case '\u30E3': //  # KATAKANA LETTER SMALL YA
    567                 case '\u30E5': //  # KATAKANA LETTER SMALL YU
    568                 case '\u30E7': //  # KATAKANA LETTER SMALL YO
    569                 case '\u30EE': //  # KATAKANA LETTER SMALL WA
    570                 case '\u30F5': //  # KATAKANA LETTER SMALL KA
    571                 case '\u30F6': //  # KATAKANA LETTER SMALL KE
    572                 case '\u30FB': //  # KATAKANA MIDDLE DOT
    573                 case '\u30FC': //  # KATAKANA-HIRAGANA PROLONGED SOUND MARK
    574                 case '\u30FD': //  # KATAKANA ITERATION MARK
    575                 case '\u30FE': //  # KATAKANA VOICED ITERATION MARK
    576                     return false;
    577                 }
    578             }
    579             return true; // Katakana (except small characters)
    580         }
    581         if (c >= '\u3400' && c <= '\u4DB5') {
    582             return true; // CJK UNIFIED IDEOGRAPHS EXTENSION A
    583         }
    584         if (c >= '\u4E00' && c <= '\u9FBB') {
    585             return true; // CJK UNIFIED IDEOGRAPHS
    586         }
    587         if (c >= '\uF900' && c <= '\uFAD9') {
    588             return true; // CJK COMPATIBILITY IDEOGRAPHS
    589         }
    590         if (c >= '\uA000' && c <= '\uA48F') {
    591             return true; // YI SYLLABLES
    592         }
    593         if (c >= '\uA490' && c <= '\uA4CF') {
    594             return true; // YI RADICALS
    595         }
    596         if (c >= '\uFE62' && c <= '\uFE66') {
    597             return true; // SMALL PLUS SIGN to SMALL EQUALS SIGN
    598         }
    599         if (c >= '\uFF10' && c <= '\uFF19') {
    600             return true; // WIDE DIGITS
    601         }
    602 
    603         return false;
    604     }
    605 
    606     private int out(CharSequence text, int start, int end,
    607                       int above, int below, int top, int bottom, int v,
    608                       float spacingmult, float spacingadd,
    609                       LineHeightSpan[] chooseHt, int[] chooseHtv,
    610                       Paint.FontMetricsInt fm, boolean hasTabOrEmoji,
    611                       boolean needMultiply, byte[] chdirs, int dir,
    612                       boolean easy, int bufEnd, boolean includePad,
    613                       boolean trackPad, char[] chs,
    614                       float[] widths, int widthStart, TextUtils.TruncateAt ellipsize,
    615                       float ellipsisWidth, float textWidth,
    616                       TextPaint paint, boolean moreChars) {
    617         int j = mLineCount;
    618         int off = j * mColumns;
    619         int want = off + mColumns + TOP;
    620         int[] lines = mLines;
    621 
    622         if (want >= lines.length) {
    623             int nlen = ArrayUtils.idealIntArraySize(want + 1);
    624             int[] grow = new int[nlen];
    625             System.arraycopy(lines, 0, grow, 0, lines.length);
    626             mLines = grow;
    627             lines = grow;
    628 
    629             Directions[] grow2 = new Directions[nlen];
    630             System.arraycopy(mLineDirections, 0, grow2, 0,
    631                              mLineDirections.length);
    632             mLineDirections = grow2;
    633         }
    634 
    635         if (chooseHt != null) {
    636             fm.ascent = above;
    637             fm.descent = below;
    638             fm.top = top;
    639             fm.bottom = bottom;
    640 
    641             for (int i = 0; i < chooseHt.length; i++) {
    642                 if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
    643                     ((LineHeightSpan.WithDensity) chooseHt[i]).
    644                         chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
    645 
    646                 } else {
    647                     chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
    648                 }
    649             }
    650 
    651             above = fm.ascent;
    652             below = fm.descent;
    653             top = fm.top;
    654             bottom = fm.bottom;
    655         }
    656 
    657         if (j == 0) {
    658             if (trackPad) {
    659                 mTopPadding = top - above;
    660             }
    661 
    662             if (includePad) {
    663                 above = top;
    664             }
    665         }
    666         if (end == bufEnd) {
    667             if (trackPad) {
    668                 mBottomPadding = bottom - below;
    669             }
    670 
    671             if (includePad) {
    672                 below = bottom;
    673             }
    674         }
    675 
    676         int extra;
    677 
    678         if (needMultiply) {
    679             double ex = (below - above) * (spacingmult - 1) + spacingadd;
    680             if (ex >= 0) {
    681                 extra = (int)(ex + EXTRA_ROUNDING);
    682             } else {
    683                 extra = -(int)(-ex + EXTRA_ROUNDING);
    684             }
    685         } else {
    686             extra = 0;
    687         }
    688 
    689         lines[off + START] = start;
    690         lines[off + TOP] = v;
    691         lines[off + DESCENT] = below + extra;
    692 
    693         v += (below - above) + extra;
    694         lines[off + mColumns + START] = end;
    695         lines[off + mColumns + TOP] = v;
    696 
    697         if (hasTabOrEmoji)
    698             lines[off + TAB] |= TAB_MASK;
    699 
    700         lines[off + DIR] |= dir << DIR_SHIFT;
    701         Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT;
    702         // easy means all chars < the first RTL, so no emoji, no nothing
    703         // XXX a run with no text or all spaces is easy but might be an empty
    704         // RTL paragraph.  Make sure easy is false if this is the case.
    705         if (easy) {
    706             mLineDirections[j] = linedirs;
    707         } else {
    708             mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs,
    709                     start - widthStart, end - start);
    710         }
    711 
    712         if (ellipsize != null) {
    713             // If there is only one line, then do any type of ellipsis except when it is MARQUEE
    714             // if there are multiple lines, just allow END ellipsis on the last line
    715             boolean firstLine = (j == 0);
    716             boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
    717             boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
    718 
    719             boolean doEllipsis = (firstLine && !moreChars &&
    720                                 ellipsize != TextUtils.TruncateAt.MARQUEE) ||
    721                         (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
    722                                 ellipsize == TextUtils.TruncateAt.END);
    723             if (doEllipsis) {
    724                 calculateEllipsis(start, end, widths, widthStart,
    725                         ellipsisWidth, ellipsize, j,
    726                         textWidth, paint, forceEllipsis);
    727             }
    728         }
    729 
    730         mLineCount++;
    731         return v;
    732     }
    733 
    734     private void calculateEllipsis(int lineStart, int lineEnd,
    735                                    float[] widths, int widthStart,
    736                                    float avail, TextUtils.TruncateAt where,
    737                                    int line, float textWidth, TextPaint paint,
    738                                    boolean forceEllipsis) {
    739         if (textWidth <= avail && !forceEllipsis) {
    740             // Everything fits!
    741             mLines[mColumns * line + ELLIPSIS_START] = 0;
    742             mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
    743             return;
    744         }
    745 
    746         float ellipsisWidth = paint.measureText(
    747                 (where == TextUtils.TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL);
    748         int ellipsisStart = 0;
    749         int ellipsisCount = 0;
    750         int len = lineEnd - lineStart;
    751 
    752         // We only support start ellipsis on a single line
    753         if (where == TextUtils.TruncateAt.START) {
    754             if (mMaximumVisibleLineCount == 1) {
    755                 float sum = 0;
    756                 int i;
    757 
    758                 for (i = len; i >= 0; i--) {
    759                     float w = widths[i - 1 + lineStart - widthStart];
    760 
    761                     if (w + sum + ellipsisWidth > avail) {
    762                         break;
    763                     }
    764 
    765                     sum += w;
    766                 }
    767 
    768                 ellipsisStart = 0;
    769                 ellipsisCount = i;
    770             } else {
    771                 if (Log.isLoggable(TAG, Log.WARN)) {
    772                     Log.w(TAG, "Start Ellipsis only supported with one line");
    773                 }
    774             }
    775         } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
    776                 where == TextUtils.TruncateAt.END_SMALL) {
    777             float sum = 0;
    778             int i;
    779 
    780             for (i = 0; i < len; i++) {
    781                 float w = widths[i + lineStart - widthStart];
    782 
    783                 if (w + sum + ellipsisWidth > avail) {
    784                     break;
    785                 }
    786 
    787                 sum += w;
    788             }
    789 
    790             ellipsisStart = i;
    791             ellipsisCount = len - i;
    792             if (forceEllipsis && ellipsisCount == 0 && len > 0) {
    793                 ellipsisStart = len - 1;
    794                 ellipsisCount = 1;
    795             }
    796         } else {
    797             // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line
    798             if (mMaximumVisibleLineCount == 1) {
    799                 float lsum = 0, rsum = 0;
    800                 int left = 0, right = len;
    801 
    802                 float ravail = (avail - ellipsisWidth) / 2;
    803                 for (right = len; right >= 0; right--) {
    804                     float w = widths[right - 1 + lineStart - widthStart];
    805 
    806                     if (w + rsum > ravail) {
    807                         break;
    808                     }
    809 
    810                     rsum += w;
    811                 }
    812 
    813                 float lavail = avail - ellipsisWidth - rsum;
    814                 for (left = 0; left < right; left++) {
    815                     float w = widths[left + lineStart - widthStart];
    816 
    817                     if (w + lsum > lavail) {
    818                         break;
    819                     }
    820 
    821                     lsum += w;
    822                 }
    823 
    824                 ellipsisStart = left;
    825                 ellipsisCount = right - left;
    826             } else {
    827                 if (Log.isLoggable(TAG, Log.WARN)) {
    828                     Log.w(TAG, "Middle Ellipsis only supported with one line");
    829                 }
    830             }
    831         }
    832 
    833         mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
    834         mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
    835     }
    836 
    837     // Override the base class so we can directly access our members,
    838     // rather than relying on member functions.
    839     // The logic mirrors that of Layout.getLineForVertical
    840     // FIXME: It may be faster to do a linear search for layouts without many lines.
    841     @Override
    842     public int getLineForVertical(int vertical) {
    843         int high = mLineCount;
    844         int low = -1;
    845         int guess;
    846         int[] lines = mLines;
    847         while (high - low > 1) {
    848             guess = (high + low) >> 1;
    849             if (lines[mColumns * guess + TOP] > vertical){
    850                 high = guess;
    851             } else {
    852                 low = guess;
    853             }
    854         }
    855         if (low < 0) {
    856             return 0;
    857         } else {
    858             return low;
    859         }
    860     }
    861 
    862     @Override
    863     public int getLineCount() {
    864         return mLineCount;
    865     }
    866 
    867     @Override
    868     public int getLineTop(int line) {
    869         int top = mLines[mColumns * line + TOP];
    870         if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount &&
    871                 line != mLineCount) {
    872             top += getBottomPadding();
    873         }
    874         return top;
    875     }
    876 
    877     @Override
    878     public int getLineDescent(int line) {
    879         int descent = mLines[mColumns * line + DESCENT];
    880         if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount - 1 && // -1 intended
    881                 line != mLineCount) {
    882             descent += getBottomPadding();
    883         }
    884         return descent;
    885     }
    886 
    887     @Override
    888     public int getLineStart(int line) {
    889         return mLines[mColumns * line + START] & START_MASK;
    890     }
    891 
    892     @Override
    893     public int getParagraphDirection(int line) {
    894         return mLines[mColumns * line + DIR] >> DIR_SHIFT;
    895     }
    896 
    897     @Override
    898     public boolean getLineContainsTab(int line) {
    899         return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
    900     }
    901 
    902     @Override
    903     public final Directions getLineDirections(int line) {
    904         return mLineDirections[line];
    905     }
    906 
    907     @Override
    908     public int getTopPadding() {
    909         return mTopPadding;
    910     }
    911 
    912     @Override
    913     public int getBottomPadding() {
    914         return mBottomPadding;
    915     }
    916 
    917     @Override
    918     public int getEllipsisCount(int line) {
    919         if (mColumns < COLUMNS_ELLIPSIZE) {
    920             return 0;
    921         }
    922 
    923         return mLines[mColumns * line + ELLIPSIS_COUNT];
    924     }
    925 
    926     @Override
    927     public int getEllipsisStart(int line) {
    928         if (mColumns < COLUMNS_ELLIPSIZE) {
    929             return 0;
    930         }
    931 
    932         return mLines[mColumns * line + ELLIPSIS_START];
    933     }
    934 
    935     @Override
    936     public int getEllipsizedWidth() {
    937         return mEllipsizedWidth;
    938     }
    939 
    940     void prepare() {
    941         mMeasured = MeasuredText.obtain();
    942     }
    943 
    944     void finish() {
    945         mMeasured = MeasuredText.recycle(mMeasured);
    946     }
    947 
    948     private int mLineCount;
    949     private int mTopPadding, mBottomPadding;
    950     private int mColumns;
    951     private int mEllipsizedWidth;
    952 
    953     private static final int COLUMNS_NORMAL = 3;
    954     private static final int COLUMNS_ELLIPSIZE = 5;
    955     private static final int START = 0;
    956     private static final int DIR = START;
    957     private static final int TAB = START;
    958     private static final int TOP = 1;
    959     private static final int DESCENT = 2;
    960     private static final int ELLIPSIS_START = 3;
    961     private static final int ELLIPSIS_COUNT = 4;
    962 
    963     private int[] mLines;
    964     private Directions[] mLineDirections;
    965     private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
    966 
    967     private static final int START_MASK = 0x1FFFFFFF;
    968     private static final int DIR_SHIFT  = 30;
    969     private static final int TAB_MASK   = 0x20000000;
    970 
    971     private static final int TAB_INCREMENT = 20; // same as Layout, but that's private
    972 
    973     private static final char CHAR_FIRST_CJK = '\u2E80';
    974 
    975     private static final char CHAR_NEW_LINE = '\n';
    976     private static final char CHAR_TAB = '\t';
    977     private static final char CHAR_SPACE = ' ';
    978     private static final char CHAR_DOT = '.';
    979     private static final char CHAR_COMMA = ',';
    980     private static final char CHAR_COLON = ':';
    981     private static final char CHAR_SEMICOLON = ';';
    982     private static final char CHAR_SLASH = '/';
    983     private static final char CHAR_HYPHEN = '-';
    984 
    985     private static final double EXTRA_ROUNDING = 0.5;
    986 
    987     private static final String ELLIPSIS_NORMAL = "\u2026"; // this is "..."
    988     private static final String ELLIPSIS_TWO_DOTS = "\u2025"; // this is ".."
    989 
    990     private static final int CHAR_FIRST_HIGH_SURROGATE = 0xD800;
    991     private static final int CHAR_LAST_LOW_SURROGATE = 0xDFFF;
    992 
    993     /*
    994      * This is reused across calls to generate()
    995      */
    996     private MeasuredText mMeasured;
    997     private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
    998 }
    999