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