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