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 import com.android.internal.util.GrowingArrayUtils;
     30 
     31 /**
     32  * StaticLayout is a Layout for text that will not be edited after it
     33  * is laid out.  Use {@link DynamicLayout} for text that may change.
     34  * <p>This is used by widgets to control text layout. You should not need
     35  * to use this class directly unless you are implementing your own widget
     36  * or custom display object, or would be tempted to call
     37  * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int,
     38  * float, float, android.graphics.Paint)
     39  * Canvas.drawText()} directly.</p>
     40  */
     41 public class StaticLayout extends Layout {
     42 
     43     static final String TAG = "StaticLayout";
     44 
     45     public StaticLayout(CharSequence source, TextPaint paint,
     46                         int width,
     47                         Alignment align, float spacingmult, float spacingadd,
     48                         boolean includepad) {
     49         this(source, 0, source.length(), paint, width, align,
     50              spacingmult, spacingadd, includepad);
     51     }
     52 
     53     /**
     54      * @hide
     55      */
     56     public StaticLayout(CharSequence source, TextPaint paint,
     57             int width, Alignment align, TextDirectionHeuristic textDir,
     58             float spacingmult, float spacingadd,
     59             boolean includepad) {
     60         this(source, 0, source.length(), paint, width, align, textDir,
     61                 spacingmult, spacingadd, includepad);
     62     }
     63 
     64     public StaticLayout(CharSequence source, int bufstart, int bufend,
     65                         TextPaint paint, int outerwidth,
     66                         Alignment align,
     67                         float spacingmult, float spacingadd,
     68                         boolean includepad) {
     69         this(source, bufstart, bufend, paint, outerwidth, align,
     70              spacingmult, spacingadd, includepad, null, 0);
     71     }
     72 
     73     /**
     74      * @hide
     75      */
     76     public StaticLayout(CharSequence source, int bufstart, int bufend,
     77             TextPaint paint, int outerwidth,
     78             Alignment align, TextDirectionHeuristic textDir,
     79             float spacingmult, float spacingadd,
     80             boolean includepad) {
     81         this(source, bufstart, bufend, paint, outerwidth, align, textDir,
     82                 spacingmult, spacingadd, includepad, null, 0, Integer.MAX_VALUE);
     83 }
     84 
     85     public StaticLayout(CharSequence source, int bufstart, int bufend,
     86             TextPaint paint, int outerwidth,
     87             Alignment align,
     88             float spacingmult, float spacingadd,
     89             boolean includepad,
     90             TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
     91         this(source, bufstart, bufend, paint, outerwidth, align,
     92                 TextDirectionHeuristics.FIRSTSTRONG_LTR,
     93                 spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
     94     }
     95 
     96     /**
     97      * @hide
     98      */
     99     public StaticLayout(CharSequence source, int bufstart, int bufend,
    100                         TextPaint paint, int outerwidth,
    101                         Alignment align, TextDirectionHeuristic textDir,
    102                         float spacingmult, float spacingadd,
    103                         boolean includepad,
    104                         TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) {
    105         super((ellipsize == null)
    106                 ? source
    107                 : (source instanceof Spanned)
    108                     ? new SpannedEllipsizer(source)
    109                     : new Ellipsizer(source),
    110               paint, outerwidth, align, textDir, spacingmult, spacingadd);
    111 
    112         /*
    113          * This is annoying, but we can't refer to the layout until
    114          * superclass construction is finished, and the superclass
    115          * constructor wants the reference to the display text.
    116          *
    117          * This will break if the superclass constructor ever actually
    118          * cares about the content instead of just holding the reference.
    119          */
    120         if (ellipsize != null) {
    121             Ellipsizer e = (Ellipsizer) getText();
    122 
    123             e.mLayout = this;
    124             e.mWidth = ellipsizedWidth;
    125             e.mMethod = ellipsize;
    126             mEllipsizedWidth = ellipsizedWidth;
    127 
    128             mColumns = COLUMNS_ELLIPSIZE;
    129         } else {
    130             mColumns = COLUMNS_NORMAL;
    131             mEllipsizedWidth = outerwidth;
    132         }
    133 
    134         mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns);
    135         mLines = new int[mLineDirections.length];
    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         mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns);
    153         mLines = new int[mLineDirections.length];
    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         int[] breakOpp = null;
    165         final String localeLanguageTag = paint.getTextLocale().toLanguageTag();
    166 
    167         mLineCount = 0;
    168 
    169         int v = 0;
    170         boolean needMultiply = (spacingmult != 1 || spacingadd != 0);
    171 
    172         Paint.FontMetricsInt fm = mFontMetricsInt;
    173         int[] chooseHtv = null;
    174 
    175         MeasuredText measured = mMeasured;
    176 
    177         Spanned spanned = null;
    178         if (source instanceof Spanned)
    179             spanned = (Spanned) source;
    180 
    181         int paraEnd;
    182         for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
    183             paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd);
    184             if (paraEnd < 0)
    185                 paraEnd = bufEnd;
    186             else
    187                 paraEnd++;
    188 
    189             int firstWidthLineLimit = mLineCount + 1;
    190             int firstWidth = outerWidth;
    191             int restWidth = outerWidth;
    192 
    193             LineHeightSpan[] chooseHt = null;
    194 
    195             if (spanned != null) {
    196                 LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd,
    197                         LeadingMarginSpan.class);
    198                 for (int i = 0; i < sp.length; i++) {
    199                     LeadingMarginSpan lms = sp[i];
    200                     firstWidth -= sp[i].getLeadingMargin(true);
    201                     restWidth -= sp[i].getLeadingMargin(false);
    202 
    203                     // LeadingMarginSpan2 is odd.  The count affects all
    204                     // leading margin spans, not just this particular one
    205                     if (lms instanceof LeadingMarginSpan2) {
    206                         LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms;
    207                         int lmsFirstLine = getLineForOffset(spanned.getSpanStart(lms2));
    208                         firstWidthLineLimit = Math.max(firstWidthLineLimit,
    209                                 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 = ArrayUtils.newUnpaddedIntArray(chooseHt.length);
    219                     }
    220 
    221                     for (int i = 0; i < chooseHt.length; i++) {
    222                         int o = spanned.getSpanStart(chooseHt[i]);
    223 
    224                         if (o < paraStart) {
    225                             // starts in this layout, before the
    226                             // current paragraph
    227 
    228                             chooseHtv[i] = getLineTop(getLineForOffset(o));
    229                         } else {
    230                             // starts in this paragraph
    231 
    232                             chooseHtv[i] = v;
    233                         }
    234                     }
    235                 }
    236             }
    237 
    238             measured.setPara(source, paraStart, paraEnd, textDir);
    239             char[] chs = measured.mChars;
    240             float[] widths = measured.mWidths;
    241             byte[] chdirs = measured.mLevels;
    242             int dir = measured.mDir;
    243             boolean easy = measured.mEasy;
    244 
    245             breakOpp = nLineBreakOpportunities(localeLanguageTag, chs, paraEnd - paraStart, breakOpp);
    246             int breakOppIndex = 0;
    247 
    248             int width = firstWidth;
    249 
    250             float w = 0;
    251             // here is the offset of the starting character of the line we are currently measuring
    252             int here = paraStart;
    253 
    254             // ok is a character offset located after a word separator (space, tab, number...) where
    255             // we would prefer to cut the current line. Equals to here when no such break was found.
    256             int ok = paraStart;
    257             float okWidth = w;
    258             int okAscent = 0, okDescent = 0, okTop = 0, okBottom = 0;
    259 
    260             // fit is a character offset such that the [here, fit[ range fits in the allowed width.
    261             // We will cut the line there if no ok position is found.
    262             int fit = paraStart;
    263             float fitWidth = w;
    264             int fitAscent = 0, fitDescent = 0, fitTop = 0, fitBottom = 0;
    265             // same as fitWidth but not including any trailing whitespace
    266             float fitWidthGraphing = w;
    267 
    268             boolean hasTabOrEmoji = false;
    269             boolean hasTab = false;
    270             TabStops tabStops = null;
    271 
    272             for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
    273 
    274                 if (spanned == null) {
    275                     spanEnd = paraEnd;
    276                     int spanLen = spanEnd - spanStart;
    277                     measured.addStyleRun(paint, spanLen, fm);
    278                 } else {
    279                     spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
    280                             MetricAffectingSpan.class);
    281                     int spanLen = spanEnd - spanStart;
    282                     MetricAffectingSpan[] spans =
    283                             spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
    284                     spans = TextUtils.removeEmptySpans(spans, spanned, MetricAffectingSpan.class);
    285                     measured.addStyleRun(paint, spans, spanLen, fm);
    286                 }
    287 
    288                 int fmTop = fm.top;
    289                 int fmBottom = fm.bottom;
    290                 int fmAscent = fm.ascent;
    291                 int fmDescent = fm.descent;
    292 
    293                 for (int j = spanStart; j < spanEnd; j++) {
    294                     char c = chs[j - paraStart];
    295 
    296                     if (c == CHAR_NEW_LINE) {
    297                         // intentionally left empty
    298                     } else if (c == CHAR_TAB) {
    299                         if (hasTab == false) {
    300                             hasTab = true;
    301                             hasTabOrEmoji = true;
    302                             if (spanned != null) {
    303                                 // First tab this para, check for tabstops
    304                                 TabStopSpan[] spans = getParagraphSpans(spanned, paraStart,
    305                                         paraEnd, TabStopSpan.class);
    306                                 if (spans.length > 0) {
    307                                     tabStops = new TabStops(TAB_INCREMENT, spans);
    308                                 }
    309                             }
    310                         }
    311                         if (tabStops != null) {
    312                             w = tabStops.nextTab(w);
    313                         } else {
    314                             w = TabStops.nextDefaultStop(w, TAB_INCREMENT);
    315                         }
    316                     } else if (c >= CHAR_FIRST_HIGH_SURROGATE && c <= CHAR_LAST_LOW_SURROGATE
    317                             && j + 1 < spanEnd) {
    318                         int emoji = Character.codePointAt(chs, j - paraStart);
    319 
    320                         if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) {
    321                             Bitmap bm = EMOJI_FACTORY.getBitmapFromAndroidPua(emoji);
    322 
    323                             if (bm != null) {
    324                                 Paint whichPaint;
    325 
    326                                 if (spanned == null) {
    327                                     whichPaint = paint;
    328                                 } else {
    329                                     whichPaint = mWorkPaint;
    330                                 }
    331 
    332                                 float wid = bm.getWidth() * -whichPaint.ascent() / bm.getHeight();
    333 
    334                                 w += wid;
    335                                 hasTabOrEmoji = true;
    336                                 j++;
    337                             } else {
    338                                 w += widths[j - paraStart];
    339                             }
    340                         } else {
    341                             w += widths[j - paraStart];
    342                         }
    343                     } else {
    344                         w += widths[j - paraStart];
    345                     }
    346 
    347                     boolean isSpaceOrTab = c == CHAR_SPACE || c == CHAR_TAB || c == CHAR_ZWSP;
    348 
    349                     if (w <= width || isSpaceOrTab) {
    350                         fitWidth = w;
    351                         if (!isSpaceOrTab) {
    352                             fitWidthGraphing = w;
    353                         }
    354                         fit = j + 1;
    355 
    356                         if (fmTop < fitTop)
    357                             fitTop = fmTop;
    358                         if (fmAscent < fitAscent)
    359                             fitAscent = fmAscent;
    360                         if (fmDescent > fitDescent)
    361                             fitDescent = fmDescent;
    362                         if (fmBottom > fitBottom)
    363                             fitBottom = fmBottom;
    364 
    365                         while (breakOpp[breakOppIndex] != -1
    366                                 && breakOpp[breakOppIndex] < j - paraStart + 1) {
    367                             breakOppIndex++;
    368                         }
    369                         boolean isLineBreak = breakOppIndex < breakOpp.length &&
    370                                 breakOpp[breakOppIndex] == j - paraStart + 1;
    371 
    372                         if (isLineBreak) {
    373                             okWidth = fitWidthGraphing;
    374                             ok = j + 1;
    375 
    376                             if (fitTop < okTop)
    377                                 okTop = fitTop;
    378                             if (fitAscent < okAscent)
    379                                 okAscent = fitAscent;
    380                             if (fitDescent > okDescent)
    381                                 okDescent = fitDescent;
    382                             if (fitBottom > okBottom)
    383                                 okBottom = fitBottom;
    384                         }
    385                     } else {
    386                         int endPos;
    387                         int above, below, top, bottom;
    388                         float currentTextWidth;
    389 
    390                         if (ok != here) {
    391                             endPos = ok;
    392                             above = okAscent;
    393                             below = okDescent;
    394                             top = okTop;
    395                             bottom = okBottom;
    396                             currentTextWidth = okWidth;
    397                         } else if (fit != here) {
    398                             endPos = fit;
    399                             above = fitAscent;
    400                             below = fitDescent;
    401                             top = fitTop;
    402                             bottom = fitBottom;
    403                             currentTextWidth = fitWidth;
    404                         } else {
    405                             // must make progress, so take next character
    406                             endPos = here + 1;
    407                             // but to deal properly with clusters
    408                             // take all zero width characters following that
    409                             while (endPos < spanEnd && widths[endPos - paraStart] == 0) {
    410                                 endPos++;
    411                             }
    412                             above = fmAscent;
    413                             below = fmDescent;
    414                             top = fmTop;
    415                             bottom = fmBottom;
    416                             currentTextWidth = widths[here - paraStart];
    417                         }
    418 
    419                         int ellipseEnd = endPos;
    420                         if (mMaximumVisibleLineCount == 1 && ellipsize == TextUtils.TruncateAt.MIDDLE) {
    421                             ellipseEnd = paraEnd;
    422                         }
    423                         v = out(source, here, ellipseEnd,
    424                                 above, below, top, bottom,
    425                                 v, spacingmult, spacingadd, chooseHt,chooseHtv, fm, hasTabOrEmoji,
    426                                 needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad,
    427                                 chs, widths, paraStart, ellipsize, ellipsizedWidth,
    428                                 currentTextWidth, paint, true);
    429 
    430                         here = endPos;
    431                         j = here - 1; // restart j-span loop from here, compensating for the j++
    432                         ok = fit = here;
    433                         w = 0;
    434                         fitWidthGraphing = w;
    435                         fitAscent = fitDescent = fitTop = fitBottom = 0;
    436                         okAscent = okDescent = okTop = okBottom = 0;
    437 
    438                         if (--firstWidthLineLimit <= 0) {
    439                             width = restWidth;
    440                         }
    441 
    442                         if (here < spanStart) {
    443                             // The text was cut before the beginning of the current span range.
    444                             // Exit the span loop, and get spanStart to start over from here.
    445                             measured.setPos(here);
    446                             spanEnd = here;
    447                             break;
    448                         }
    449 
    450                         if (mLineCount >= mMaximumVisibleLineCount) {
    451                             return;
    452                         }
    453                     }
    454                 }
    455             }
    456 
    457             if (paraEnd != here && mLineCount < mMaximumVisibleLineCount) {
    458                 if ((fitTop | fitBottom | fitDescent | fitAscent) == 0) {
    459                     paint.getFontMetricsInt(fm);
    460 
    461                     fitTop = fm.top;
    462                     fitBottom = fm.bottom;
    463                     fitAscent = fm.ascent;
    464                     fitDescent = fm.descent;
    465                 }
    466 
    467                 // Log.e("text", "output rest " + here + " to " + end);
    468 
    469                 v = out(source,
    470                         here, paraEnd, fitAscent, fitDescent,
    471                         fitTop, fitBottom,
    472                         v,
    473                         spacingmult, spacingadd, chooseHt,
    474                         chooseHtv, fm, hasTabOrEmoji,
    475                         needMultiply, chdirs, dir, easy, bufEnd,
    476                         includepad, trackpad, chs,
    477                         widths, paraStart, ellipsize,
    478                         ellipsizedWidth, w, paint, paraEnd != bufEnd);
    479             }
    480 
    481             paraStart = paraEnd;
    482 
    483             if (paraEnd == bufEnd)
    484                 break;
    485         }
    486 
    487         if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) &&
    488                 mLineCount < mMaximumVisibleLineCount) {
    489             // Log.e("text", "output last " + bufEnd);
    490 
    491             measured.setPara(source, bufStart, bufEnd, textDir);
    492 
    493             paint.getFontMetricsInt(fm);
    494 
    495             v = out(source,
    496                     bufEnd, bufEnd, fm.ascent, fm.descent,
    497                     fm.top, fm.bottom,
    498                     v,
    499                     spacingmult, spacingadd, null,
    500                     null, fm, false,
    501                     needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd,
    502                     includepad, trackpad, null,
    503                     null, bufStart, ellipsize,
    504                     ellipsizedWidth, 0, paint, false);
    505         }
    506     }
    507 
    508     private int out(CharSequence text, int start, int end,
    509                       int above, int below, int top, int bottom, int v,
    510                       float spacingmult, float spacingadd,
    511                       LineHeightSpan[] chooseHt, int[] chooseHtv,
    512                       Paint.FontMetricsInt fm, boolean hasTabOrEmoji,
    513                       boolean needMultiply, byte[] chdirs, int dir,
    514                       boolean easy, int bufEnd, boolean includePad,
    515                       boolean trackPad, char[] chs,
    516                       float[] widths, int widthStart, TextUtils.TruncateAt ellipsize,
    517                       float ellipsisWidth, float textWidth,
    518                       TextPaint paint, boolean moreChars) {
    519         int j = mLineCount;
    520         int off = j * mColumns;
    521         int want = off + mColumns + TOP;
    522         int[] lines = mLines;
    523 
    524         if (want >= lines.length) {
    525             Directions[] grow2 = ArrayUtils.newUnpaddedArray(
    526                     Directions.class, GrowingArrayUtils.growSize(want));
    527             System.arraycopy(mLineDirections, 0, grow2, 0,
    528                              mLineDirections.length);
    529             mLineDirections = grow2;
    530 
    531             int[] grow = new int[grow2.length];
    532             System.arraycopy(lines, 0, grow, 0, lines.length);
    533             mLines = grow;
    534             lines = grow;
    535         }
    536 
    537         if (chooseHt != null) {
    538             fm.ascent = above;
    539             fm.descent = below;
    540             fm.top = top;
    541             fm.bottom = bottom;
    542 
    543             for (int i = 0; i < chooseHt.length; i++) {
    544                 if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
    545                     ((LineHeightSpan.WithDensity) chooseHt[i]).
    546                         chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
    547 
    548                 } else {
    549                     chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
    550                 }
    551             }
    552 
    553             above = fm.ascent;
    554             below = fm.descent;
    555             top = fm.top;
    556             bottom = fm.bottom;
    557         }
    558 
    559         boolean firstLine = (j == 0);
    560         boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
    561         boolean lastLine = currentLineIsTheLastVisibleOne || (end == bufEnd);
    562 
    563         if (firstLine) {
    564             if (trackPad) {
    565                 mTopPadding = top - above;
    566             }
    567 
    568             if (includePad) {
    569                 above = top;
    570             }
    571         }
    572 
    573         int extra;
    574 
    575         if (lastLine) {
    576             if (trackPad) {
    577                 mBottomPadding = bottom - below;
    578             }
    579 
    580             if (includePad) {
    581                 below = bottom;
    582             }
    583         }
    584 
    585 
    586         if (needMultiply && !lastLine) {
    587             double ex = (below - above) * (spacingmult - 1) + spacingadd;
    588             if (ex >= 0) {
    589                 extra = (int)(ex + EXTRA_ROUNDING);
    590             } else {
    591                 extra = -(int)(-ex + EXTRA_ROUNDING);
    592             }
    593         } else {
    594             extra = 0;
    595         }
    596 
    597         lines[off + START] = start;
    598         lines[off + TOP] = v;
    599         lines[off + DESCENT] = below + extra;
    600 
    601         v += (below - above) + extra;
    602         lines[off + mColumns + START] = end;
    603         lines[off + mColumns + TOP] = v;
    604 
    605         if (hasTabOrEmoji)
    606             lines[off + TAB] |= TAB_MASK;
    607 
    608         lines[off + DIR] |= dir << DIR_SHIFT;
    609         Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT;
    610         // easy means all chars < the first RTL, so no emoji, no nothing
    611         // XXX a run with no text or all spaces is easy but might be an empty
    612         // RTL paragraph.  Make sure easy is false if this is the case.
    613         if (easy) {
    614             mLineDirections[j] = linedirs;
    615         } else {
    616             mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs,
    617                     start - widthStart, end - start);
    618         }
    619 
    620         if (ellipsize != null) {
    621             // If there is only one line, then do any type of ellipsis except when it is MARQUEE
    622             // if there are multiple lines, just allow END ellipsis on the last line
    623             boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
    624 
    625             boolean doEllipsis =
    626                         (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) &&
    627                                 ellipsize != TextUtils.TruncateAt.MARQUEE) ||
    628                         (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
    629                                 ellipsize == TextUtils.TruncateAt.END);
    630             if (doEllipsis) {
    631                 calculateEllipsis(start, end, widths, widthStart,
    632                         ellipsisWidth, ellipsize, j,
    633                         textWidth, paint, forceEllipsis);
    634             }
    635         }
    636 
    637         mLineCount++;
    638         return v;
    639     }
    640 
    641     private void calculateEllipsis(int lineStart, int lineEnd,
    642                                    float[] widths, int widthStart,
    643                                    float avail, TextUtils.TruncateAt where,
    644                                    int line, float textWidth, TextPaint paint,
    645                                    boolean forceEllipsis) {
    646         if (textWidth <= avail && !forceEllipsis) {
    647             // Everything fits!
    648             mLines[mColumns * line + ELLIPSIS_START] = 0;
    649             mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
    650             return;
    651         }
    652 
    653         float ellipsisWidth = paint.measureText(
    654                 (where == TextUtils.TruncateAt.END_SMALL) ?
    655                         TextUtils.ELLIPSIS_TWO_DOTS : TextUtils.ELLIPSIS_NORMAL, 0, 1);
    656         int ellipsisStart = 0;
    657         int ellipsisCount = 0;
    658         int len = lineEnd - lineStart;
    659 
    660         // We only support start ellipsis on a single line
    661         if (where == TextUtils.TruncateAt.START) {
    662             if (mMaximumVisibleLineCount == 1) {
    663                 float sum = 0;
    664                 int i;
    665 
    666                 for (i = len; i >= 0; i--) {
    667                     float w = widths[i - 1 + lineStart - widthStart];
    668 
    669                     if (w + sum + ellipsisWidth > avail) {
    670                         break;
    671                     }
    672 
    673                     sum += w;
    674                 }
    675 
    676                 ellipsisStart = 0;
    677                 ellipsisCount = i;
    678             } else {
    679                 if (Log.isLoggable(TAG, Log.WARN)) {
    680                     Log.w(TAG, "Start Ellipsis only supported with one line");
    681                 }
    682             }
    683         } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
    684                 where == TextUtils.TruncateAt.END_SMALL) {
    685             float sum = 0;
    686             int i;
    687 
    688             for (i = 0; i < len; i++) {
    689                 float w = widths[i + lineStart - widthStart];
    690 
    691                 if (w + sum + ellipsisWidth > avail) {
    692                     break;
    693                 }
    694 
    695                 sum += w;
    696             }
    697 
    698             ellipsisStart = i;
    699             ellipsisCount = len - i;
    700             if (forceEllipsis && ellipsisCount == 0 && len > 0) {
    701                 ellipsisStart = len - 1;
    702                 ellipsisCount = 1;
    703             }
    704         } else {
    705             // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line
    706             if (mMaximumVisibleLineCount == 1) {
    707                 float lsum = 0, rsum = 0;
    708                 int left = 0, right = len;
    709 
    710                 float ravail = (avail - ellipsisWidth) / 2;
    711                 for (right = len; right > 0; right--) {
    712                     float w = widths[right - 1 + lineStart - widthStart];
    713 
    714                     if (w + rsum > ravail) {
    715                         break;
    716                     }
    717 
    718                     rsum += w;
    719                 }
    720 
    721                 float lavail = avail - ellipsisWidth - rsum;
    722                 for (left = 0; left < right; left++) {
    723                     float w = widths[left + lineStart - widthStart];
    724 
    725                     if (w + lsum > lavail) {
    726                         break;
    727                     }
    728 
    729                     lsum += w;
    730                 }
    731 
    732                 ellipsisStart = left;
    733                 ellipsisCount = right - left;
    734             } else {
    735                 if (Log.isLoggable(TAG, Log.WARN)) {
    736                     Log.w(TAG, "Middle Ellipsis only supported with one line");
    737                 }
    738             }
    739         }
    740 
    741         mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
    742         mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
    743     }
    744 
    745     // Override the base class so we can directly access our members,
    746     // rather than relying on member functions.
    747     // The logic mirrors that of Layout.getLineForVertical
    748     // FIXME: It may be faster to do a linear search for layouts without many lines.
    749     @Override
    750     public int getLineForVertical(int vertical) {
    751         int high = mLineCount;
    752         int low = -1;
    753         int guess;
    754         int[] lines = mLines;
    755         while (high - low > 1) {
    756             guess = (high + low) >> 1;
    757             if (lines[mColumns * guess + TOP] > vertical){
    758                 high = guess;
    759             } else {
    760                 low = guess;
    761             }
    762         }
    763         if (low < 0) {
    764             return 0;
    765         } else {
    766             return low;
    767         }
    768     }
    769 
    770     @Override
    771     public int getLineCount() {
    772         return mLineCount;
    773     }
    774 
    775     @Override
    776     public int getLineTop(int line) {
    777         int top = mLines[mColumns * line + TOP];
    778         if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount &&
    779                 line != mLineCount) {
    780             top += getBottomPadding();
    781         }
    782         return top;
    783     }
    784 
    785     @Override
    786     public int getLineDescent(int line) {
    787         int descent = mLines[mColumns * line + DESCENT];
    788         if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount - 1 && // -1 intended
    789                 line != mLineCount) {
    790             descent += getBottomPadding();
    791         }
    792         return descent;
    793     }
    794 
    795     @Override
    796     public int getLineStart(int line) {
    797         return mLines[mColumns * line + START] & START_MASK;
    798     }
    799 
    800     @Override
    801     public int getParagraphDirection(int line) {
    802         return mLines[mColumns * line + DIR] >> DIR_SHIFT;
    803     }
    804 
    805     @Override
    806     public boolean getLineContainsTab(int line) {
    807         return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
    808     }
    809 
    810     @Override
    811     public final Directions getLineDirections(int line) {
    812         return mLineDirections[line];
    813     }
    814 
    815     @Override
    816     public int getTopPadding() {
    817         return mTopPadding;
    818     }
    819 
    820     @Override
    821     public int getBottomPadding() {
    822         return mBottomPadding;
    823     }
    824 
    825     @Override
    826     public int getEllipsisCount(int line) {
    827         if (mColumns < COLUMNS_ELLIPSIZE) {
    828             return 0;
    829         }
    830 
    831         return mLines[mColumns * line + ELLIPSIS_COUNT];
    832     }
    833 
    834     @Override
    835     public int getEllipsisStart(int line) {
    836         if (mColumns < COLUMNS_ELLIPSIZE) {
    837             return 0;
    838         }
    839 
    840         return mLines[mColumns * line + ELLIPSIS_START];
    841     }
    842 
    843     @Override
    844     public int getEllipsizedWidth() {
    845         return mEllipsizedWidth;
    846     }
    847 
    848     void prepare() {
    849         mMeasured = MeasuredText.obtain();
    850     }
    851 
    852     void finish() {
    853         mMeasured = MeasuredText.recycle(mMeasured);
    854     }
    855 
    856     // returns an array with terminal sentinel value -1 to indicate end
    857     // this is so that arrays can be recycled instead of allocating new arrays
    858     // every time
    859     private static native int[] nLineBreakOpportunities(String locale, char[] text, int length, int[] recycle);
    860 
    861     private int mLineCount;
    862     private int mTopPadding, mBottomPadding;
    863     private int mColumns;
    864     private int mEllipsizedWidth;
    865 
    866     private static final int COLUMNS_NORMAL = 3;
    867     private static final int COLUMNS_ELLIPSIZE = 5;
    868     private static final int START = 0;
    869     private static final int DIR = START;
    870     private static final int TAB = START;
    871     private static final int TOP = 1;
    872     private static final int DESCENT = 2;
    873     private static final int ELLIPSIS_START = 3;
    874     private static final int ELLIPSIS_COUNT = 4;
    875 
    876     private int[] mLines;
    877     private Directions[] mLineDirections;
    878     private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
    879 
    880     private static final int START_MASK = 0x1FFFFFFF;
    881     private static final int DIR_SHIFT  = 30;
    882     private static final int TAB_MASK   = 0x20000000;
    883 
    884     private static final int TAB_INCREMENT = 20; // same as Layout, but that's private
    885 
    886     private static final char CHAR_NEW_LINE = '\n';
    887     private static final char CHAR_TAB = '\t';
    888     private static final char CHAR_SPACE = ' ';
    889     private static final char CHAR_ZWSP = '\u200B';
    890 
    891     private static final double EXTRA_ROUNDING = 0.5;
    892 
    893     private static final int CHAR_FIRST_HIGH_SURROGATE = 0xD800;
    894     private static final int CHAR_LAST_LOW_SURROGATE = 0xDFFF;
    895 
    896     /*
    897      * This is reused across calls to generate()
    898      */
    899     private MeasuredText mMeasured;
    900     private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
    901 }
    902