StaticLayout.java revision ad0b051b133baf92f199c96a8ac1e81b3393190c
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 != null) {
733            // If there is only one line, then do any type of ellipsis except when it is MARQUEE
734            // if there are multiple lines, just allow END ellipsis on the last line
735            boolean firstLine = (j == 0);
736            boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
737            boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
738
739            boolean doEllipsis = (firstLine && !moreChars &&
740                                ellipsize != TextUtils.TruncateAt.MARQUEE) ||
741                        (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
742                                ellipsize == TextUtils.TruncateAt.END);
743            if (doEllipsis) {
744                calculateEllipsis(start, end, widths, widthStart,
745                        ellipsisWidth, ellipsize, j,
746                        textWidth, paint, forceEllipsis);
747            }
748        }
749
750        mLineCount++;
751        return v;
752    }
753
754    private void calculateEllipsis(int lineStart, int lineEnd,
755                                   float[] widths, int widthStart,
756                                   float avail, TextUtils.TruncateAt where,
757                                   int line, float textWidth, TextPaint paint,
758                                   boolean forceEllipsis) {
759        if (textWidth <= avail && !forceEllipsis) {
760            // Everything fits!
761            mLines[mColumns * line + ELLIPSIS_START] = 0;
762            mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
763            return;
764        }
765
766        float ellipsisWidth = paint.measureText(
767                (where == TextUtils.TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL);
768        int ellipsisStart = 0;
769        int ellipsisCount = 0;
770        int len = lineEnd - lineStart;
771
772        // We only support start ellipsis on a single line
773        if (where == TextUtils.TruncateAt.START) {
774            if (mMaximumVisibleLineCount == 1) {
775                float sum = 0;
776                int i;
777
778                for (i = len; i >= 0; i--) {
779                    float w = widths[i - 1 + lineStart - widthStart];
780
781                    if (w + sum + ellipsisWidth > avail) {
782                        break;
783                    }
784
785                    sum += w;
786                }
787
788                ellipsisStart = 0;
789                ellipsisCount = i;
790            } else {
791                if (Log.isLoggable(TAG, Log.WARN)) {
792                    Log.w(TAG, "Start Ellipsis only supported with one line");
793                }
794            }
795        } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
796                where == TextUtils.TruncateAt.END_SMALL) {
797            float sum = 0;
798            int i;
799
800            for (i = 0; i < len; i++) {
801                float w = widths[i + lineStart - widthStart];
802
803                if (w + sum + ellipsisWidth > avail) {
804                    break;
805                }
806
807                sum += w;
808            }
809
810            ellipsisStart = i;
811            ellipsisCount = len - i;
812            if (forceEllipsis && ellipsisCount == 0 && len > 0) {
813                ellipsisStart = len - 1;
814                ellipsisCount = 1;
815            }
816        } else {
817            // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line
818            if (mMaximumVisibleLineCount == 1) {
819                float lsum = 0, rsum = 0;
820                int left = 0, right = len;
821
822                float ravail = (avail - ellipsisWidth) / 2;
823                for (right = len; right >= 0; right--) {
824                    float w = widths[right - 1 + lineStart - widthStart];
825
826                    if (w + rsum > ravail) {
827                        break;
828                    }
829
830                    rsum += w;
831                }
832
833                float lavail = avail - ellipsisWidth - rsum;
834                for (left = 0; left < right; left++) {
835                    float w = widths[left + lineStart - widthStart];
836
837                    if (w + lsum > lavail) {
838                        break;
839                    }
840
841                    lsum += w;
842                }
843
844                ellipsisStart = left;
845                ellipsisCount = right - left;
846            } else {
847                if (Log.isLoggable(TAG, Log.WARN)) {
848                    Log.w(TAG, "Middle Ellipsis only supported with one line");
849                }
850            }
851        }
852
853        mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
854        mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
855    }
856
857    // Override the base class so we can directly access our members,
858    // rather than relying on member functions.
859    // The logic mirrors that of Layout.getLineForVertical
860    // FIXME: It may be faster to do a linear search for layouts without many lines.
861    @Override
862    public int getLineForVertical(int vertical) {
863        int high = mLineCount;
864        int low = -1;
865        int guess;
866        int[] lines = mLines;
867        while (high - low > 1) {
868            guess = (high + low) >> 1;
869            if (lines[mColumns * guess + TOP] > vertical){
870                high = guess;
871            } else {
872                low = guess;
873            }
874        }
875        if (low < 0) {
876            return 0;
877        } else {
878            return low;
879        }
880    }
881
882    @Override
883    public int getLineCount() {
884        return mLineCount;
885    }
886
887    @Override
888    public int getLineTop(int line) {
889        int top = mLines[mColumns * line + TOP];
890        if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount &&
891                line != mLineCount) {
892            top += getBottomPadding();
893        }
894        return top;
895    }
896
897    @Override
898    public int getLineDescent(int line) {
899        int descent = mLines[mColumns * line + DESCENT];
900        if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount - 1 && // -1 intended
901                line != mLineCount) {
902            descent += getBottomPadding();
903        }
904        return descent;
905    }
906
907    @Override
908    public int getLineStart(int line) {
909        return mLines[mColumns * line + START] & START_MASK;
910    }
911
912    @Override
913    public int getParagraphDirection(int line) {
914        return mLines[mColumns * line + DIR] >> DIR_SHIFT;
915    }
916
917    @Override
918    public boolean getLineContainsTab(int line) {
919        return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
920    }
921
922    @Override
923    public final Directions getLineDirections(int line) {
924        return mLineDirections[line];
925    }
926
927    @Override
928    public int getTopPadding() {
929        return mTopPadding;
930    }
931
932    @Override
933    public int getBottomPadding() {
934        return mBottomPadding;
935    }
936
937    @Override
938    public int getEllipsisCount(int line) {
939        if (mColumns < COLUMNS_ELLIPSIZE) {
940            return 0;
941        }
942
943        return mLines[mColumns * line + ELLIPSIS_COUNT];
944    }
945
946    @Override
947    public int getEllipsisStart(int line) {
948        if (mColumns < COLUMNS_ELLIPSIZE) {
949            return 0;
950        }
951
952        return mLines[mColumns * line + ELLIPSIS_START];
953    }
954
955    @Override
956    public int getEllipsizedWidth() {
957        return mEllipsizedWidth;
958    }
959
960    void prepare() {
961        mMeasured = MeasuredText.obtain();
962    }
963
964    void finish() {
965        mMeasured = MeasuredText.recycle(mMeasured);
966    }
967
968    private int mLineCount;
969    private int mTopPadding, mBottomPadding;
970    private int mColumns;
971    private int mEllipsizedWidth;
972
973    private static final int COLUMNS_NORMAL = 3;
974    private static final int COLUMNS_ELLIPSIZE = 5;
975    private static final int START = 0;
976    private static final int DIR = START;
977    private static final int TAB = START;
978    private static final int TOP = 1;
979    private static final int DESCENT = 2;
980    private static final int ELLIPSIS_START = 3;
981    private static final int ELLIPSIS_COUNT = 4;
982
983    private int[] mLines;
984    private Directions[] mLineDirections;
985    private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
986
987    private static final int START_MASK = 0x1FFFFFFF;
988    private static final int DIR_SHIFT  = 30;
989    private static final int TAB_MASK   = 0x20000000;
990
991    private static final int TAB_INCREMENT = 20; // same as Layout, but that's private
992
993    private static final char CHAR_FIRST_CJK = '\u2E80';
994
995    private static final char CHAR_NEW_LINE = '\n';
996    private static final char CHAR_TAB = '\t';
997    private static final char CHAR_SPACE = ' ';
998    private static final char CHAR_DOT = '.';
999    private static final char CHAR_COMMA = ',';
1000    private static final char CHAR_COLON = ':';
1001    private static final char CHAR_SEMICOLON = ';';
1002    private static final char CHAR_SLASH = '/';
1003    private static final char CHAR_HYPHEN = '-';
1004
1005    private static final double EXTRA_ROUNDING = 0.5;
1006
1007    private static final String ELLIPSIS_NORMAL = "\u2026"; // this is "..."
1008    private static final String ELLIPSIS_TWO_DOTS = "\u2025"; // this is ".."
1009
1010    private static final int CHAR_FIRST_HIGH_SURROGATE = 0xD800;
1011    private static final int CHAR_LAST_LOW_SURROGATE = 0xDFFF;
1012
1013    /*
1014     * This is reused across calls to generate()
1015     */
1016    private MeasuredText mMeasured;
1017    private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
1018}
1019