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[ArrayUtils.idealIntArraySize(2 * mColumns)];
    154         // FIXME This is never recycled
    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             // here is the offset of the starting character of the line we are currently measuring
    250             int here = paraStart;
    251 
    252             // ok is a character offset located after a word separator (space, tab, number...) where
    253             // we would prefer to cut the current line. Equals to here when no such break was found.
    254             int ok = paraStart;
    255             float okWidth = w;
    256             int okAscent = 0, okDescent = 0, okTop = 0, okBottom = 0;
    257 
    258             // fit is a character offset such that the [here, fit[ range fits in the allowed width.
    259             // We will cut the line there if no ok position is found.
    260             int fit = paraStart;
    261             float fitWidth = w;
    262             int fitAscent = 0, fitDescent = 0, fitTop = 0, fitBottom = 0;
    263 
    264             boolean hasTabOrEmoji = false;
    265             boolean hasTab = false;
    266             TabStops tabStops = null;
    267 
    268             for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
    269 
    270                 if (spanned == null) {
    271                     spanEnd = paraEnd;
    272                     int spanLen = spanEnd - spanStart;
    273                     measured.addStyleRun(paint, spanLen, fm);
    274                 } else {
    275                     spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
    276                             MetricAffectingSpan.class);
    277                     int spanLen = spanEnd - spanStart;
    278                     MetricAffectingSpan[] spans =
    279                             spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
    280                     spans = TextUtils.removeEmptySpans(spans, spanned, MetricAffectingSpan.class);
    281                     measured.addStyleRun(paint, spans, spanLen, fm);
    282                 }
    283 
    284                 int fmTop = fm.top;
    285                 int fmBottom = fm.bottom;
    286                 int fmAscent = fm.ascent;
    287                 int fmDescent = fm.descent;
    288 
    289                 for (int j = spanStart; j < spanEnd; j++) {
    290                     char c = chs[j - paraStart];
    291 
    292                     if (c == CHAR_NEW_LINE) {
    293                         // intentionally left empty
    294                     } else if (c == CHAR_TAB) {
    295                         if (hasTab == false) {
    296                             hasTab = true;
    297                             hasTabOrEmoji = true;
    298                             if (spanned != null) {
    299                                 // First tab this para, check for tabstops
    300                                 TabStopSpan[] spans = getParagraphSpans(spanned, paraStart,
    301                                         paraEnd, TabStopSpan.class);
    302                                 if (spans.length > 0) {
    303                                     tabStops = new TabStops(TAB_INCREMENT, spans);
    304                                 }
    305                             }
    306                         }
    307                         if (tabStops != null) {
    308                             w = tabStops.nextTab(w);
    309                         } else {
    310                             w = TabStops.nextDefaultStop(w, TAB_INCREMENT);
    311                         }
    312                     } else if (c >= CHAR_FIRST_HIGH_SURROGATE && c <= CHAR_LAST_LOW_SURROGATE
    313                             && j + 1 < spanEnd) {
    314                         int emoji = Character.codePointAt(chs, j - paraStart);
    315 
    316                         if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) {
    317                             Bitmap bm = EMOJI_FACTORY.getBitmapFromAndroidPua(emoji);
    318 
    319                             if (bm != null) {
    320                                 Paint whichPaint;
    321 
    322                                 if (spanned == null) {
    323                                     whichPaint = paint;
    324                                 } else {
    325                                     whichPaint = mWorkPaint;
    326                                 }
    327 
    328                                 float wid = bm.getWidth() * -whichPaint.ascent() / bm.getHeight();
    329 
    330                                 w += wid;
    331                                 hasTabOrEmoji = true;
    332                                 j++;
    333                             } else {
    334                                 w += widths[j - paraStart];
    335                             }
    336                         } else {
    337                             w += widths[j - paraStart];
    338                         }
    339                     } else {
    340                         w += widths[j - paraStart];
    341                     }
    342 
    343                     boolean isSpaceOrTab = c == CHAR_SPACE || c == CHAR_TAB || c == CHAR_ZWSP;
    344 
    345                     if (w <= width || isSpaceOrTab) {
    346                         fitWidth = w;
    347                         fit = j + 1;
    348 
    349                         if (fmTop < fitTop)
    350                             fitTop = fmTop;
    351                         if (fmAscent < fitAscent)
    352                             fitAscent = fmAscent;
    353                         if (fmDescent > fitDescent)
    354                             fitDescent = fmDescent;
    355                         if (fmBottom > fitBottom)
    356                             fitBottom = fmBottom;
    357 
    358                         // From the Unicode Line Breaking Algorithm (at least approximately)
    359                         boolean isLineBreak = isSpaceOrTab ||
    360                                 // / is class SY and - is class HY, except when followed by a digit
    361                                 ((c == CHAR_SLASH || c == CHAR_HYPHEN) &&
    362                                 (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) ||
    363                                 // Ideographs are class ID: breakpoints when adjacent, except for NS
    364                                 // (non-starters), which can be broken after but not before
    365                                 (c >= CHAR_FIRST_CJK && isIdeographic(c, true) &&
    366                                 j + 1 < spanEnd && isIdeographic(chs[j + 1 - paraStart], false));
    367 
    368                         if (isLineBreak) {
    369                             okWidth = w;
    370                             ok = j + 1;
    371 
    372                             if (fitTop < okTop)
    373                                 okTop = fitTop;
    374                             if (fitAscent < okAscent)
    375                                 okAscent = fitAscent;
    376                             if (fitDescent > okDescent)
    377                                 okDescent = fitDescent;
    378                             if (fitBottom > okBottom)
    379                                 okBottom = fitBottom;
    380                         }
    381                     } else {
    382                         final boolean moreChars = (j + 1 < spanEnd);
    383                         int endPos;
    384                         int above, below, top, bottom;
    385                         float currentTextWidth;
    386 
    387                         if (ok != here) {
    388                             endPos = ok;
    389                             above = okAscent;
    390                             below = okDescent;
    391                             top = okTop;
    392                             bottom = okBottom;
    393                             currentTextWidth = okWidth;
    394                         } else if (fit != here) {
    395                             endPos = fit;
    396                             above = fitAscent;
    397                             below = fitDescent;
    398                             top = fitTop;
    399                             bottom = fitBottom;
    400                             currentTextWidth = fitWidth;
    401                         } else {
    402                             endPos = here + 1;
    403                             above = fm.ascent;
    404                             below = fm.descent;
    405                             top = fm.top;
    406                             bottom = fm.bottom;
    407                             currentTextWidth = widths[here - paraStart];
    408                         }
    409 
    410                         v = out(source, here, endPos,
    411                                 above, below, top, bottom,
    412                                 v, spacingmult, spacingadd, chooseHt,chooseHtv, fm, hasTabOrEmoji,
    413                                 needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad,
    414                                 chs, widths, paraStart, ellipsize, ellipsizedWidth,
    415                                 currentTextWidth, paint, moreChars);
    416 
    417                         here = endPos;
    418                         j = here - 1; // restart j-span loop from here, compensating for the j++
    419                         ok = fit = here;
    420                         w = 0;
    421                         fitAscent = fitDescent = fitTop = fitBottom = 0;
    422                         okAscent = okDescent = okTop = okBottom = 0;
    423 
    424                         if (--firstWidthLineLimit <= 0) {
    425                             width = restWidth;
    426                         }
    427 
    428                         if (here < spanStart) {
    429                             // The text was cut before the beginning of the current span range.
    430                             // Exit the span loop, and get spanStart to start over from here.
    431                             measured.setPos(here);
    432                             spanEnd = here;
    433                             break;
    434                         }
    435 
    436                         if (mLineCount >= mMaximumVisibleLineCount) {
    437                             break;
    438                         }
    439                     }
    440                 }
    441             }
    442 
    443             if (paraEnd != here && mLineCount < mMaximumVisibleLineCount) {
    444                 if ((fitTop | fitBottom | fitDescent | fitAscent) == 0) {
    445                     paint.getFontMetricsInt(fm);
    446 
    447                     fitTop = fm.top;
    448                     fitBottom = fm.bottom;
    449                     fitAscent = fm.ascent;
    450                     fitDescent = fm.descent;
    451                 }
    452 
    453                 // Log.e("text", "output rest " + here + " to " + end);
    454 
    455                 v = out(source,
    456                         here, paraEnd, fitAscent, fitDescent,
    457                         fitTop, fitBottom,
    458                         v,
    459                         spacingmult, spacingadd, chooseHt,
    460                         chooseHtv, fm, hasTabOrEmoji,
    461                         needMultiply, chdirs, dir, easy, bufEnd,
    462                         includepad, trackpad, chs,
    463                         widths, paraStart, ellipsize,
    464                         ellipsizedWidth, w, paint, paraEnd != bufEnd);
    465             }
    466 
    467             paraStart = paraEnd;
    468 
    469             if (paraEnd == bufEnd)
    470                 break;
    471         }
    472 
    473         if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) &&
    474                 mLineCount < mMaximumVisibleLineCount) {
    475             // Log.e("text", "output last " + bufEnd);
    476 
    477             measured.setPara(source, bufStart, bufEnd, textDir);
    478 
    479             paint.getFontMetricsInt(fm);
    480 
    481             v = out(source,
    482                     bufEnd, bufEnd, fm.ascent, fm.descent,
    483                     fm.top, fm.bottom,
    484                     v,
    485                     spacingmult, spacingadd, null,
    486                     null, fm, false,
    487                     needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd,
    488                     includepad, trackpad, null,
    489                     null, bufStart, ellipsize,
    490                     ellipsizedWidth, 0, paint, false);
    491         }
    492     }
    493 
    494     /**
    495      * Returns true if the specified character is one of those specified
    496      * as being Ideographic (class ID) by the Unicode Line Breaking Algorithm
    497      * (http://www.unicode.org/unicode/reports/tr14/), and is therefore OK
    498      * to break between a pair of.
    499      *
    500      * @param includeNonStarters also return true for category NS
    501      *                           (non-starters), which can be broken
    502      *                           after but not before.
    503      */
    504     private static final boolean isIdeographic(char c, boolean includeNonStarters) {
    505         if (c >= '\u2E80' && c <= '\u2FFF') {
    506             return true; // CJK, KANGXI RADICALS, DESCRIPTION SYMBOLS
    507         }
    508         if (c == '\u3000') {
    509             return true; // IDEOGRAPHIC SPACE
    510         }
    511         if (c >= '\u3040' && c <= '\u309F') {
    512             if (!includeNonStarters) {
    513                 switch (c) {
    514                 case '\u3041': //  # HIRAGANA LETTER SMALL A
    515                 case '\u3043': //  # HIRAGANA LETTER SMALL I
    516                 case '\u3045': //  # HIRAGANA LETTER SMALL U
    517                 case '\u3047': //  # HIRAGANA LETTER SMALL E
    518                 case '\u3049': //  # HIRAGANA LETTER SMALL O
    519                 case '\u3063': //  # HIRAGANA LETTER SMALL TU
    520                 case '\u3083': //  # HIRAGANA LETTER SMALL YA
    521                 case '\u3085': //  # HIRAGANA LETTER SMALL YU
    522                 case '\u3087': //  # HIRAGANA LETTER SMALL YO
    523                 case '\u308E': //  # HIRAGANA LETTER SMALL WA
    524                 case '\u3095': //  # HIRAGANA LETTER SMALL KA
    525                 case '\u3096': //  # HIRAGANA LETTER SMALL KE
    526                 case '\u309B': //  # KATAKANA-HIRAGANA VOICED SOUND MARK
    527                 case '\u309C': //  # KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK
    528                 case '\u309D': //  # HIRAGANA ITERATION MARK
    529                 case '\u309E': //  # HIRAGANA VOICED ITERATION MARK
    530                     return false;
    531                 }
    532             }
    533             return true; // Hiragana (except small characters)
    534         }
    535         if (c >= '\u30A0' && c <= '\u30FF') {
    536             if (!includeNonStarters) {
    537                 switch (c) {
    538                 case '\u30A0': //  # KATAKANA-HIRAGANA DOUBLE HYPHEN
    539                 case '\u30A1': //  # KATAKANA LETTER SMALL A
    540                 case '\u30A3': //  # KATAKANA LETTER SMALL I
    541                 case '\u30A5': //  # KATAKANA LETTER SMALL U
    542                 case '\u30A7': //  # KATAKANA LETTER SMALL E
    543                 case '\u30A9': //  # KATAKANA LETTER SMALL O
    544                 case '\u30C3': //  # KATAKANA LETTER SMALL TU
    545                 case '\u30E3': //  # KATAKANA LETTER SMALL YA
    546                 case '\u30E5': //  # KATAKANA LETTER SMALL YU
    547                 case '\u30E7': //  # KATAKANA LETTER SMALL YO
    548                 case '\u30EE': //  # KATAKANA LETTER SMALL WA
    549                 case '\u30F5': //  # KATAKANA LETTER SMALL KA
    550                 case '\u30F6': //  # KATAKANA LETTER SMALL KE
    551                 case '\u30FB': //  # KATAKANA MIDDLE DOT
    552                 case '\u30FC': //  # KATAKANA-HIRAGANA PROLONGED SOUND MARK
    553                 case '\u30FD': //  # KATAKANA ITERATION MARK
    554                 case '\u30FE': //  # KATAKANA VOICED ITERATION MARK
    555                     return false;
    556                 }
    557             }
    558             return true; // Katakana (except small characters)
    559         }
    560         if (c >= '\u3400' && c <= '\u4DB5') {
    561             return true; // CJK UNIFIED IDEOGRAPHS EXTENSION A
    562         }
    563         if (c >= '\u4E00' && c <= '\u9FBB') {
    564             return true; // CJK UNIFIED IDEOGRAPHS
    565         }
    566         if (c >= '\uF900' && c <= '\uFAD9') {
    567             return true; // CJK COMPATIBILITY IDEOGRAPHS
    568         }
    569         if (c >= '\uA000' && c <= '\uA48F') {
    570             return true; // YI SYLLABLES
    571         }
    572         if (c >= '\uA490' && c <= '\uA4CF') {
    573             return true; // YI RADICALS
    574         }
    575         if (c >= '\uFE62' && c <= '\uFE66') {
    576             return true; // SMALL PLUS SIGN to SMALL EQUALS SIGN
    577         }
    578         if (c >= '\uFF10' && c <= '\uFF19') {
    579             return true; // WIDE DIGITS
    580         }
    581 
    582         return false;
    583     }
    584 
    585     private int out(CharSequence text, int start, int end,
    586                       int above, int below, int top, int bottom, int v,
    587                       float spacingmult, float spacingadd,
    588                       LineHeightSpan[] chooseHt, int[] chooseHtv,
    589                       Paint.FontMetricsInt fm, boolean hasTabOrEmoji,
    590                       boolean needMultiply, byte[] chdirs, int dir,
    591                       boolean easy, int bufEnd, boolean includePad,
    592                       boolean trackPad, char[] chs,
    593                       float[] widths, int widthStart, TextUtils.TruncateAt ellipsize,
    594                       float ellipsisWidth, float textWidth,
    595                       TextPaint paint, boolean moreChars) {
    596         int j = mLineCount;
    597         int off = j * mColumns;
    598         int want = off + mColumns + TOP;
    599         int[] lines = mLines;
    600 
    601         if (want >= lines.length) {
    602             int nlen = ArrayUtils.idealIntArraySize(want + 1);
    603             int[] grow = new int[nlen];
    604             System.arraycopy(lines, 0, grow, 0, lines.length);
    605             mLines = grow;
    606             lines = grow;
    607 
    608             Directions[] grow2 = new Directions[nlen];
    609             System.arraycopy(mLineDirections, 0, grow2, 0,
    610                              mLineDirections.length);
    611             mLineDirections = grow2;
    612         }
    613 
    614         if (chooseHt != null) {
    615             fm.ascent = above;
    616             fm.descent = below;
    617             fm.top = top;
    618             fm.bottom = bottom;
    619 
    620             for (int i = 0; i < chooseHt.length; i++) {
    621                 if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
    622                     ((LineHeightSpan.WithDensity) chooseHt[i]).
    623                         chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
    624 
    625                 } else {
    626                     chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
    627                 }
    628             }
    629 
    630             above = fm.ascent;
    631             below = fm.descent;
    632             top = fm.top;
    633             bottom = fm.bottom;
    634         }
    635 
    636         if (j == 0) {
    637             if (trackPad) {
    638                 mTopPadding = top - above;
    639             }
    640 
    641             if (includePad) {
    642                 above = top;
    643             }
    644         }
    645         if (end == bufEnd) {
    646             if (trackPad) {
    647                 mBottomPadding = bottom - below;
    648             }
    649 
    650             if (includePad) {
    651                 below = bottom;
    652             }
    653         }
    654 
    655         int extra;
    656 
    657         if (needMultiply) {
    658             double ex = (below - above) * (spacingmult - 1) + spacingadd;
    659             if (ex >= 0) {
    660                 extra = (int)(ex + EXTRA_ROUNDING);
    661             } else {
    662                 extra = -(int)(-ex + EXTRA_ROUNDING);
    663             }
    664         } else {
    665             extra = 0;
    666         }
    667 
    668         lines[off + START] = start;
    669         lines[off + TOP] = v;
    670         lines[off + DESCENT] = below + extra;
    671 
    672         v += (below - above) + extra;
    673         lines[off + mColumns + START] = end;
    674         lines[off + mColumns + TOP] = v;
    675 
    676         if (hasTabOrEmoji)
    677             lines[off + TAB] |= TAB_MASK;
    678 
    679         lines[off + DIR] |= dir << DIR_SHIFT;
    680         Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT;
    681         // easy means all chars < the first RTL, so no emoji, no nothing
    682         // XXX a run with no text or all spaces is easy but might be an empty
    683         // RTL paragraph.  Make sure easy is false if this is the case.
    684         if (easy) {
    685             mLineDirections[j] = linedirs;
    686         } else {
    687             mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs,
    688                     start - widthStart, end - start);
    689         }
    690 
    691         if (ellipsize != null) {
    692             // If there is only one line, then do any type of ellipsis except when it is MARQUEE
    693             // if there are multiple lines, just allow END ellipsis on the last line
    694             boolean firstLine = (j == 0);
    695             boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
    696             boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
    697 
    698             boolean doEllipsis =
    699                         (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) &&
    700                                 ellipsize != TextUtils.TruncateAt.MARQUEE) ||
    701                         (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
    702                                 ellipsize == TextUtils.TruncateAt.END);
    703             if (doEllipsis) {
    704                 calculateEllipsis(start, end, widths, widthStart,
    705                         ellipsisWidth, ellipsize, j,
    706                         textWidth, paint, forceEllipsis);
    707             }
    708         }
    709 
    710         mLineCount++;
    711         return v;
    712     }
    713 
    714     private void calculateEllipsis(int lineStart, int lineEnd,
    715                                    float[] widths, int widthStart,
    716                                    float avail, TextUtils.TruncateAt where,
    717                                    int line, float textWidth, TextPaint paint,
    718                                    boolean forceEllipsis) {
    719         if (textWidth <= avail && !forceEllipsis) {
    720             // Everything fits!
    721             mLines[mColumns * line + ELLIPSIS_START] = 0;
    722             mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
    723             return;
    724         }
    725 
    726         float ellipsisWidth = paint.measureText(
    727                 (where == TextUtils.TruncateAt.END_SMALL) ?
    728                         ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL, 0, 1);
    729         int ellipsisStart = 0;
    730         int ellipsisCount = 0;
    731         int len = lineEnd - lineStart;
    732 
    733         // We only support start ellipsis on a single line
    734         if (where == TextUtils.TruncateAt.START) {
    735             if (mMaximumVisibleLineCount == 1) {
    736                 float sum = 0;
    737                 int i;
    738 
    739                 for (i = len; i >= 0; i--) {
    740                     float w = widths[i - 1 + lineStart - widthStart];
    741 
    742                     if (w + sum + ellipsisWidth > avail) {
    743                         break;
    744                     }
    745 
    746                     sum += w;
    747                 }
    748 
    749                 ellipsisStart = 0;
    750                 ellipsisCount = i;
    751             } else {
    752                 if (Log.isLoggable(TAG, Log.WARN)) {
    753                     Log.w(TAG, "Start Ellipsis only supported with one line");
    754                 }
    755             }
    756         } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
    757                 where == TextUtils.TruncateAt.END_SMALL) {
    758             float sum = 0;
    759             int i;
    760 
    761             for (i = 0; i < len; i++) {
    762                 float w = widths[i + lineStart - widthStart];
    763 
    764                 if (w + sum + ellipsisWidth > avail) {
    765                     break;
    766                 }
    767 
    768                 sum += w;
    769             }
    770 
    771             ellipsisStart = i;
    772             ellipsisCount = len - i;
    773             if (forceEllipsis && ellipsisCount == 0 && len > 0) {
    774                 ellipsisStart = len - 1;
    775                 ellipsisCount = 1;
    776             }
    777         } else {
    778             // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line
    779             if (mMaximumVisibleLineCount == 1) {
    780                 float lsum = 0, rsum = 0;
    781                 int left = 0, right = len;
    782 
    783                 float ravail = (avail - ellipsisWidth) / 2;
    784                 for (right = len; right >= 0; right--) {
    785                     float w = widths[right - 1 + lineStart - widthStart];
    786 
    787                     if (w + rsum > ravail) {
    788                         break;
    789                     }
    790 
    791                     rsum += w;
    792                 }
    793 
    794                 float lavail = avail - ellipsisWidth - rsum;
    795                 for (left = 0; left < right; left++) {
    796                     float w = widths[left + lineStart - widthStart];
    797 
    798                     if (w + lsum > lavail) {
    799                         break;
    800                     }
    801 
    802                     lsum += w;
    803                 }
    804 
    805                 ellipsisStart = left;
    806                 ellipsisCount = right - left;
    807             } else {
    808                 if (Log.isLoggable(TAG, Log.WARN)) {
    809                     Log.w(TAG, "Middle Ellipsis only supported with one line");
    810                 }
    811             }
    812         }
    813 
    814         mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
    815         mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
    816     }
    817 
    818     // Override the base class so we can directly access our members,
    819     // rather than relying on member functions.
    820     // The logic mirrors that of Layout.getLineForVertical
    821     // FIXME: It may be faster to do a linear search for layouts without many lines.
    822     @Override
    823     public int getLineForVertical(int vertical) {
    824         int high = mLineCount;
    825         int low = -1;
    826         int guess;
    827         int[] lines = mLines;
    828         while (high - low > 1) {
    829             guess = (high + low) >> 1;
    830             if (lines[mColumns * guess + TOP] > vertical){
    831                 high = guess;
    832             } else {
    833                 low = guess;
    834             }
    835         }
    836         if (low < 0) {
    837             return 0;
    838         } else {
    839             return low;
    840         }
    841     }
    842 
    843     @Override
    844     public int getLineCount() {
    845         return mLineCount;
    846     }
    847 
    848     @Override
    849     public int getLineTop(int line) {
    850         int top = mLines[mColumns * line + TOP];
    851         if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount &&
    852                 line != mLineCount) {
    853             top += getBottomPadding();
    854         }
    855         return top;
    856     }
    857 
    858     @Override
    859     public int getLineDescent(int line) {
    860         int descent = mLines[mColumns * line + DESCENT];
    861         if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount - 1 && // -1 intended
    862                 line != mLineCount) {
    863             descent += getBottomPadding();
    864         }
    865         return descent;
    866     }
    867 
    868     @Override
    869     public int getLineStart(int line) {
    870         return mLines[mColumns * line + START] & START_MASK;
    871     }
    872 
    873     @Override
    874     public int getParagraphDirection(int line) {
    875         return mLines[mColumns * line + DIR] >> DIR_SHIFT;
    876     }
    877 
    878     @Override
    879     public boolean getLineContainsTab(int line) {
    880         return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
    881     }
    882 
    883     @Override
    884     public final Directions getLineDirections(int line) {
    885         return mLineDirections[line];
    886     }
    887 
    888     @Override
    889     public int getTopPadding() {
    890         return mTopPadding;
    891     }
    892 
    893     @Override
    894     public int getBottomPadding() {
    895         return mBottomPadding;
    896     }
    897 
    898     @Override
    899     public int getEllipsisCount(int line) {
    900         if (mColumns < COLUMNS_ELLIPSIZE) {
    901             return 0;
    902         }
    903 
    904         return mLines[mColumns * line + ELLIPSIS_COUNT];
    905     }
    906 
    907     @Override
    908     public int getEllipsisStart(int line) {
    909         if (mColumns < COLUMNS_ELLIPSIZE) {
    910             return 0;
    911         }
    912 
    913         return mLines[mColumns * line + ELLIPSIS_START];
    914     }
    915 
    916     @Override
    917     public int getEllipsizedWidth() {
    918         return mEllipsizedWidth;
    919     }
    920 
    921     void prepare() {
    922         mMeasured = MeasuredText.obtain();
    923     }
    924 
    925     void finish() {
    926         mMeasured = MeasuredText.recycle(mMeasured);
    927     }
    928 
    929     private int mLineCount;
    930     private int mTopPadding, mBottomPadding;
    931     private int mColumns;
    932     private int mEllipsizedWidth;
    933 
    934     private static final int COLUMNS_NORMAL = 3;
    935     private static final int COLUMNS_ELLIPSIZE = 5;
    936     private static final int START = 0;
    937     private static final int DIR = START;
    938     private static final int TAB = START;
    939     private static final int TOP = 1;
    940     private static final int DESCENT = 2;
    941     private static final int ELLIPSIS_START = 3;
    942     private static final int ELLIPSIS_COUNT = 4;
    943 
    944     private int[] mLines;
    945     private Directions[] mLineDirections;
    946     private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
    947 
    948     private static final int START_MASK = 0x1FFFFFFF;
    949     private static final int DIR_SHIFT  = 30;
    950     private static final int TAB_MASK   = 0x20000000;
    951 
    952     private static final int TAB_INCREMENT = 20; // same as Layout, but that's private
    953 
    954     private static final char CHAR_FIRST_CJK = '\u2E80';
    955 
    956     private static final char CHAR_NEW_LINE = '\n';
    957     private static final char CHAR_TAB = '\t';
    958     private static final char CHAR_SPACE = ' ';
    959     private static final char CHAR_SLASH = '/';
    960     private static final char CHAR_HYPHEN = '-';
    961     private static final char CHAR_ZWSP = '\u200B';
    962 
    963     private static final double EXTRA_ROUNDING = 0.5;
    964 
    965     private static final int CHAR_FIRST_HIGH_SURROGATE = 0xD800;
    966     private static final int CHAR_LAST_LOW_SURROGATE = 0xDFFF;
    967 
    968     /*
    969      * This is reused across calls to generate()
    970      */
    971     private MeasuredText mMeasured;
    972     private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
    973 }
    974