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