1/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.text;
18
19import android.graphics.Bitmap;
20import android.graphics.Paint;
21import android.text.style.LeadingMarginSpan;
22import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
23import android.text.style.LineHeightSpan;
24import android.text.style.MetricAffectingSpan;
25import android.text.style.TabStopSpan;
26import android.util.Log;
27
28import com.android.internal.util.ArrayUtils;
29
30/**
31 * StaticLayout is a Layout for text that will not be edited after it
32 * is laid out.  Use {@link DynamicLayout} for text that may change.
33 * <p>This is used by widgets to control text layout. You should not need
34 * to use this class directly unless you are implementing your own widget
35 * or custom display object, or would be tempted to call
36 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int,
37 * float, float, android.graphics.Paint)
38 * Canvas.drawText()} directly.</p>
39 */
40public class StaticLayout extends Layout {
41
42    static final String TAG = "StaticLayout";
43
44    public StaticLayout(CharSequence source, TextPaint paint,
45                        int width,
46                        Alignment align, float spacingmult, float spacingadd,
47                        boolean includepad) {
48        this(source, 0, source.length(), paint, width, align,
49             spacingmult, spacingadd, includepad);
50    }
51
52    /**
53     * @hide
54     */
55    public StaticLayout(CharSequence source, TextPaint paint,
56            int width, Alignment align, TextDirectionHeuristic textDir,
57            float spacingmult, float spacingadd,
58            boolean includepad) {
59        this(source, 0, source.length(), paint, width, align, textDir,
60                spacingmult, spacingadd, includepad);
61    }
62
63    public StaticLayout(CharSequence source, int bufstart, int bufend,
64                        TextPaint paint, int outerwidth,
65                        Alignment align,
66                        float spacingmult, float spacingadd,
67                        boolean includepad) {
68        this(source, bufstart, bufend, paint, outerwidth, align,
69             spacingmult, spacingadd, includepad, null, 0);
70    }
71
72    /**
73     * @hide
74     */
75    public StaticLayout(CharSequence source, int bufstart, int bufend,
76            TextPaint paint, int outerwidth,
77            Alignment align, TextDirectionHeuristic textDir,
78            float spacingmult, float spacingadd,
79            boolean includepad) {
80        this(source, bufstart, bufend, paint, outerwidth, align, textDir,
81                spacingmult, spacingadd, includepad, null, 0, Integer.MAX_VALUE);
82}
83
84    public StaticLayout(CharSequence source, int bufstart, int bufend,
85            TextPaint paint, int outerwidth,
86            Alignment align,
87            float spacingmult, float spacingadd,
88            boolean includepad,
89            TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
90        this(source, bufstart, bufend, paint, outerwidth, align,
91                TextDirectionHeuristics.FIRSTSTRONG_LTR,
92                spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
93    }
94
95    /**
96     * @hide
97     */
98    public StaticLayout(CharSequence source, int bufstart, int bufend,
99                        TextPaint paint, int outerwidth,
100                        Alignment align, TextDirectionHeuristic textDir,
101                        float spacingmult, float spacingadd,
102                        boolean includepad,
103                        TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) {
104        super((ellipsize == null)
105                ? source
106                : (source instanceof Spanned)
107                    ? new SpannedEllipsizer(source)
108                    : new Ellipsizer(source),
109              paint, outerwidth, align, textDir, spacingmult, spacingadd);
110
111        /*
112         * This is annoying, but we can't refer to the layout until
113         * superclass construction is finished, and the superclass
114         * constructor wants the reference to the display text.
115         *
116         * This will break if the superclass constructor ever actually
117         * cares about the content instead of just holding the reference.
118         */
119        if (ellipsize != null) {
120            Ellipsizer e = (Ellipsizer) getText();
121
122            e.mLayout = this;
123            e.mWidth = ellipsizedWidth;
124            e.mMethod = ellipsize;
125            mEllipsizedWidth = ellipsizedWidth;
126
127            mColumns = COLUMNS_ELLIPSIZE;
128        } else {
129            mColumns = COLUMNS_NORMAL;
130            mEllipsizedWidth = outerwidth;
131        }
132
133        mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)];
134        mLineDirections = new Directions[
135                             ArrayUtils.idealIntArraySize(2 * mColumns)];
136        mMaximumVisibleLineCount = maxLines;
137
138        mMeasured = MeasuredText.obtain();
139
140        generate(source, bufstart, bufend, paint, outerwidth, textDir, spacingmult,
141                 spacingadd, includepad, includepad, ellipsizedWidth,
142                 ellipsize);
143
144        mMeasured = MeasuredText.recycle(mMeasured);
145        mFontMetricsInt = null;
146    }
147
148    /* package */ StaticLayout(CharSequence text) {
149        super(text, null, 0, null, 0, 0);
150
151        mColumns = COLUMNS_ELLIPSIZE;
152        mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)];
153        mLineDirections = new Directions[
154                             ArrayUtils.idealIntArraySize(2 * mColumns)];
155        mMeasured = MeasuredText.obtain();
156    }
157
158    /* package */ void generate(CharSequence source, int bufStart, int bufEnd,
159                        TextPaint paint, int outerWidth,
160                        TextDirectionHeuristic textDir, float spacingmult,
161                        float spacingadd, boolean includepad,
162                        boolean trackpad, float ellipsizedWidth,
163                        TextUtils.TruncateAt ellipsize) {
164        mLineCount = 0;
165
166        int v = 0;
167        boolean needMultiply = (spacingmult != 1 || spacingadd != 0);
168
169        Paint.FontMetricsInt fm = mFontMetricsInt;
170        int[] chooseHtv = null;
171
172        MeasuredText measured = mMeasured;
173
174        Spanned spanned = null;
175        if (source instanceof Spanned)
176            spanned = (Spanned) source;
177
178        int DEFAULT_DIR = DIR_LEFT_TO_RIGHT; // XXX
179
180        int paraEnd;
181        for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
182            paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd);
183            if (paraEnd < 0)
184                paraEnd = bufEnd;
185            else
186                paraEnd++;
187
188            int firstWidthLineLimit = mLineCount + 1;
189            int firstWidth = outerWidth;
190            int restWidth = outerWidth;
191
192            LineHeightSpan[] chooseHt = null;
193
194            if (spanned != null) {
195                LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd,
196                        LeadingMarginSpan.class);
197                for (int i = 0; i < sp.length; i++) {
198                    LeadingMarginSpan lms = sp[i];
199                    firstWidth -= sp[i].getLeadingMargin(true);
200                    restWidth -= sp[i].getLeadingMargin(false);
201
202                    // LeadingMarginSpan2 is odd.  The count affects all
203                    // leading margin spans, not just this particular one,
204                    // and start from the top of the span, not the top of the
205                    // paragraph.
206                    if (lms instanceof LeadingMarginSpan2) {
207                        LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms;
208                        int lmsFirstLine = getLineForOffset(spanned.getSpanStart(lms2));
209                        firstWidthLineLimit = lmsFirstLine + lms2.getLeadingMarginLineCount();
210                    }
211                }
212
213                chooseHt = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class);
214
215                if (chooseHt.length != 0) {
216                    if (chooseHtv == null ||
217                        chooseHtv.length < chooseHt.length) {
218                        chooseHtv = new int[ArrayUtils.idealIntArraySize(
219                                            chooseHt.length)];
220                    }
221
222                    for (int i = 0; i < chooseHt.length; i++) {
223                        int o = spanned.getSpanStart(chooseHt[i]);
224
225                        if (o < paraStart) {
226                            // starts in this layout, before the
227                            // current paragraph
228
229                            chooseHtv[i] = getLineTop(getLineForOffset(o));
230                        } else {
231                            // starts in this paragraph
232
233                            chooseHtv[i] = v;
234                        }
235                    }
236                }
237            }
238
239            measured.setPara(source, paraStart, paraEnd, textDir);
240            char[] chs = measured.mChars;
241            float[] widths = measured.mWidths;
242            byte[] chdirs = measured.mLevels;
243            int dir = measured.mDir;
244            boolean easy = measured.mEasy;
245
246            int width = firstWidth;
247
248            float w = 0;
249            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() * -whichPaint.ascent() / bm.getHeight();
332
333                                w += wid;
334                                hasTabOrEmoji = true;
335                                j++;
336                            } else {
337                                w += widths[j - paraStart];
338                            }
339                        } else {
340                            w += widths[j - paraStart];
341                        }
342                    } else {
343                        w += widths[j - paraStart];
344                    }
345
346                    // Log.e("text", "was " + before + " now " + w + " after " + c + " within " + width);
347
348                    if (w <= width) {
349                        fitWidth = w;
350                        fit = j + 1;
351
352                        if (fmTop < fitTop)
353                            fitTop = fmTop;
354                        if (fmAscent < fitAscent)
355                            fitAscent = fmAscent;
356                        if (fmDescent > fitDescent)
357                            fitDescent = fmDescent;
358                        if (fmBottom > fitBottom)
359                            fitBottom = fmBottom;
360
361                        /*
362                         * From the Unicode Line Breaking Algorithm:
363                         * (at least approximately)
364                         *
365                         * .,:; are class IS: breakpoints
366                         *      except when adjacent to digits
367                         * /    is class SY: a breakpoint
368                         *      except when followed by a digit.
369                         * -    is class HY: a breakpoint
370                         *      except when followed by a digit.
371                         *
372                         * Ideographs are class ID: breakpoints when adjacent,
373                         * except for NS (non-starters), which can be broken
374                         * after but not before.
375                         */
376
377                        if (c == CHAR_SPACE || c == CHAR_TAB ||
378                            ((c == CHAR_DOT || c == CHAR_COMMA ||
379                                    c == CHAR_COLON || c == CHAR_SEMICOLON) &&
380                             (j - 1 < here || !Character.isDigit(chs[j - 1 - paraStart])) &&
381                             (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) ||
382                            ((c == CHAR_SLASH || c == CHAR_HYPHEN) &&
383                             (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) ||
384                            (c >= CHAR_FIRST_CJK && isIdeographic(c, true) &&
385                             j + 1 < spanEnd && isIdeographic(chs[j + 1 - paraStart], false))) {
386                            okWidth = w;
387                            ok = j + 1;
388
389                            if (fitTop < okTop)
390                                okTop = fitTop;
391                            if (fitAscent < okAscent)
392                                okAscent = fitAscent;
393                            if (fitDescent > okDescent)
394                                okDescent = fitDescent;
395                            if (fitBottom > okBottom)
396                                okBottom = fitBottom;
397                        }
398                    } else {
399                        final boolean moreChars = (j + 1 < spanEnd);
400                        int endPos;
401                        int above, below, top, bottom;
402                        float currentTextWidth;
403
404                        if (ok != here) {
405                            // If it is a space that makes the length exceed width, cut here
406                            if (c == CHAR_SPACE) ok = j + 1;
407
408                            while (ok < spanEnd && chs[ok - paraStart] == CHAR_SPACE) {
409                                ok++;
410                            }
411
412                            endPos = ok;
413                            above = okAscent;
414                            below = okDescent;
415                            top = okTop;
416                            bottom = okBottom;
417                            currentTextWidth = okWidth;
418                        } else if (fit != here) {
419                            endPos = fit;
420                            above = fitAscent;
421                            below = fitDescent;
422                            top = fitTop;
423                            bottom = fitBottom;
424                            currentTextWidth = fitWidth;
425                        } else {
426                            endPos = here + 1;
427                            above = fm.ascent;
428                            below = fm.descent;
429                            top = fm.top;
430                            bottom = fm.bottom;
431                            currentTextWidth = widths[here - paraStart];
432                        }
433
434                        v = out(source, here, endPos,
435                                above, below, top, bottom,
436                                v, spacingmult, spacingadd, chooseHt,chooseHtv, fm, hasTabOrEmoji,
437                                needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad,
438                                chs, widths, paraStart, ellipsize, ellipsizedWidth,
439                                currentTextWidth, paint, moreChars);
440                        here = endPos;
441
442                        if (here < spanStart) {
443                            // didn't output all the text for this span
444                            // we've measured the raw widths, though, so
445                            // just reset the start point
446                            j = nextSpanStart = here;
447                        } else {
448                            j = here - 1;    // continue looping
449                        }
450
451                        ok = fit = here;
452                        w = 0;
453                        fitAscent = fitDescent = fitTop = fitBottom = 0;
454                        okAscent = okDescent = okTop = okBottom = 0;
455
456                        if (--firstWidthLineLimit <= 0) {
457                            width = restWidth;
458                        }
459                    }
460                    if (mLineCount >= mMaximumVisibleLineCount) {
461                        break;
462                    }
463                }
464            }
465
466            if (paraEnd != here && mLineCount < mMaximumVisibleLineCount) {
467                if ((fitTop | fitBottom | fitDescent | fitAscent) == 0) {
468                    paint.getFontMetricsInt(fm);
469
470                    fitTop = fm.top;
471                    fitBottom = fm.bottom;
472                    fitAscent = fm.ascent;
473                    fitDescent = fm.descent;
474                }
475
476                // Log.e("text", "output rest " + here + " to " + end);
477
478                v = out(source,
479                        here, paraEnd, fitAscent, fitDescent,
480                        fitTop, fitBottom,
481                        v,
482                        spacingmult, spacingadd, chooseHt,
483                        chooseHtv, fm, hasTabOrEmoji,
484                        needMultiply, chdirs, dir, easy, bufEnd,
485                        includepad, trackpad, chs,
486                        widths, paraStart, ellipsize,
487                        ellipsizedWidth, w, paint, paraEnd != bufEnd);
488            }
489
490            paraStart = paraEnd;
491
492            if (paraEnd == bufEnd)
493                break;
494        }
495
496        if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) &&
497                mLineCount < mMaximumVisibleLineCount) {
498            // Log.e("text", "output last " + bufEnd);
499
500            paint.getFontMetricsInt(fm);
501
502            v = out(source,
503                    bufEnd, bufEnd, fm.ascent, fm.descent,
504                    fm.top, fm.bottom,
505                    v,
506                    spacingmult, spacingadd, null,
507                    null, fm, false,
508                    needMultiply, null, DEFAULT_DIR, true, bufEnd,
509                    includepad, trackpad, null,
510                    null, bufStart, ellipsize,
511                    ellipsizedWidth, 0, paint, false);
512        }
513    }
514
515    /**
516     * Returns true if the specified character is one of those specified
517     * as being Ideographic (class ID) by the Unicode Line Breaking Algorithm
518     * (http://www.unicode.org/unicode/reports/tr14/), and is therefore OK
519     * to break between a pair of.
520     *
521     * @param includeNonStarters also return true for category NS
522     *                           (non-starters), which can be broken
523     *                           after but not before.
524     */
525    private static final boolean isIdeographic(char c, boolean includeNonStarters) {
526        if (c >= '\u2E80' && c <= '\u2FFF') {
527            return true; // CJK, KANGXI RADICALS, DESCRIPTION SYMBOLS
528        }
529        if (c == '\u3000') {
530            return true; // IDEOGRAPHIC SPACE
531        }
532        if (c >= '\u3040' && c <= '\u309F') {
533            if (!includeNonStarters) {
534                switch (c) {
535                case '\u3041': //  # HIRAGANA LETTER SMALL A
536                case '\u3043': //  # HIRAGANA LETTER SMALL I
537                case '\u3045': //  # HIRAGANA LETTER SMALL U
538                case '\u3047': //  # HIRAGANA LETTER SMALL E
539                case '\u3049': //  # HIRAGANA LETTER SMALL O
540                case '\u3063': //  # HIRAGANA LETTER SMALL TU
541                case '\u3083': //  # HIRAGANA LETTER SMALL YA
542                case '\u3085': //  # HIRAGANA LETTER SMALL YU
543                case '\u3087': //  # HIRAGANA LETTER SMALL YO
544                case '\u308E': //  # HIRAGANA LETTER SMALL WA
545                case '\u3095': //  # HIRAGANA LETTER SMALL KA
546                case '\u3096': //  # HIRAGANA LETTER SMALL KE
547                case '\u309B': //  # KATAKANA-HIRAGANA VOICED SOUND MARK
548                case '\u309C': //  # KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK
549                case '\u309D': //  # HIRAGANA ITERATION MARK
550                case '\u309E': //  # HIRAGANA VOICED ITERATION MARK
551                    return false;
552                }
553            }
554            return true; // Hiragana (except small characters)
555        }
556        if (c >= '\u30A0' && c <= '\u30FF') {
557            if (!includeNonStarters) {
558                switch (c) {
559                case '\u30A0': //  # KATAKANA-HIRAGANA DOUBLE HYPHEN
560                case '\u30A1': //  # KATAKANA LETTER SMALL A
561                case '\u30A3': //  # KATAKANA LETTER SMALL I
562                case '\u30A5': //  # KATAKANA LETTER SMALL U
563                case '\u30A7': //  # KATAKANA LETTER SMALL E
564                case '\u30A9': //  # KATAKANA LETTER SMALL O
565                case '\u30C3': //  # KATAKANA LETTER SMALL TU
566                case '\u30E3': //  # KATAKANA LETTER SMALL YA
567                case '\u30E5': //  # KATAKANA LETTER SMALL YU
568                case '\u30E7': //  # KATAKANA LETTER SMALL YO
569                case '\u30EE': //  # KATAKANA LETTER SMALL WA
570                case '\u30F5': //  # KATAKANA LETTER SMALL KA
571                case '\u30F6': //  # KATAKANA LETTER SMALL KE
572                case '\u30FB': //  # KATAKANA MIDDLE DOT
573                case '\u30FC': //  # KATAKANA-HIRAGANA PROLONGED SOUND MARK
574                case '\u30FD': //  # KATAKANA ITERATION MARK
575                case '\u30FE': //  # KATAKANA VOICED ITERATION MARK
576                    return false;
577                }
578            }
579            return true; // Katakana (except small characters)
580        }
581        if (c >= '\u3400' && c <= '\u4DB5') {
582            return true; // CJK UNIFIED IDEOGRAPHS EXTENSION A
583        }
584        if (c >= '\u4E00' && c <= '\u9FBB') {
585            return true; // CJK UNIFIED IDEOGRAPHS
586        }
587        if (c >= '\uF900' && c <= '\uFAD9') {
588            return true; // CJK COMPATIBILITY IDEOGRAPHS
589        }
590        if (c >= '\uA000' && c <= '\uA48F') {
591            return true; // YI SYLLABLES
592        }
593        if (c >= '\uA490' && c <= '\uA4CF') {
594            return true; // YI RADICALS
595        }
596        if (c >= '\uFE62' && c <= '\uFE66') {
597            return true; // SMALL PLUS SIGN to SMALL EQUALS SIGN
598        }
599        if (c >= '\uFF10' && c <= '\uFF19') {
600            return true; // WIDE DIGITS
601        }
602
603        return false;
604    }
605
606    private int out(CharSequence text, int start, int end,
607                      int above, int below, int top, int bottom, int v,
608                      float spacingmult, float spacingadd,
609                      LineHeightSpan[] chooseHt, int[] chooseHtv,
610                      Paint.FontMetricsInt fm, boolean hasTabOrEmoji,
611                      boolean needMultiply, byte[] chdirs, int dir,
612                      boolean easy, int bufEnd, boolean includePad,
613                      boolean trackPad, char[] chs,
614                      float[] widths, int widthStart, TextUtils.TruncateAt ellipsize,
615                      float ellipsisWidth, float textWidth,
616                      TextPaint paint, boolean moreChars) {
617        int j = mLineCount;
618        int off = j * mColumns;
619        int want = off + mColumns + TOP;
620        int[] lines = mLines;
621
622        if (want >= lines.length) {
623            int nlen = ArrayUtils.idealIntArraySize(want + 1);
624            int[] grow = new int[nlen];
625            System.arraycopy(lines, 0, grow, 0, lines.length);
626            mLines = grow;
627            lines = grow;
628
629            Directions[] grow2 = new Directions[nlen];
630            System.arraycopy(mLineDirections, 0, grow2, 0,
631                             mLineDirections.length);
632            mLineDirections = grow2;
633        }
634
635        if (chooseHt != null) {
636            fm.ascent = above;
637            fm.descent = below;
638            fm.top = top;
639            fm.bottom = bottom;
640
641            for (int i = 0; i < chooseHt.length; i++) {
642                if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
643                    ((LineHeightSpan.WithDensity) chooseHt[i]).
644                        chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
645
646                } else {
647                    chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
648                }
649            }
650
651            above = fm.ascent;
652            below = fm.descent;
653            top = fm.top;
654            bottom = fm.bottom;
655        }
656
657        if (j == 0) {
658            if (trackPad) {
659                mTopPadding = top - above;
660            }
661
662            if (includePad) {
663                above = top;
664            }
665        }
666        if (end == bufEnd) {
667            if (trackPad) {
668                mBottomPadding = bottom - below;
669            }
670
671            if (includePad) {
672                below = bottom;
673            }
674        }
675
676        int extra;
677
678        if (needMultiply) {
679            double ex = (below - above) * (spacingmult - 1) + spacingadd;
680            if (ex >= 0) {
681                extra = (int)(ex + EXTRA_ROUNDING);
682            } else {
683                extra = -(int)(-ex + EXTRA_ROUNDING);
684            }
685        } else {
686            extra = 0;
687        }
688
689        lines[off + START] = start;
690        lines[off + TOP] = v;
691        lines[off + DESCENT] = below + extra;
692
693        v += (below - above) + extra;
694        lines[off + mColumns + START] = end;
695        lines[off + mColumns + TOP] = v;
696
697        if (hasTabOrEmoji)
698            lines[off + TAB] |= TAB_MASK;
699
700        lines[off + DIR] |= dir << DIR_SHIFT;
701        Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT;
702        // easy means all chars < the first RTL, so no emoji, no nothing
703        // XXX a run with no text or all spaces is easy but might be an empty
704        // RTL paragraph.  Make sure easy is false if this is the case.
705        if (easy) {
706            mLineDirections[j] = linedirs;
707        } else {
708            mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs,
709                    start - widthStart, end - start);
710        }
711
712        if (ellipsize != null) {
713            // If there is only one line, then do any type of ellipsis except when it is MARQUEE
714            // if there are multiple lines, just allow END ellipsis on the last line
715            boolean firstLine = (j == 0);
716            boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
717            boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
718
719            boolean doEllipsis = (firstLine && !moreChars &&
720                                ellipsize != TextUtils.TruncateAt.MARQUEE) ||
721                        (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
722                                ellipsize == TextUtils.TruncateAt.END);
723            if (doEllipsis) {
724                calculateEllipsis(start, end, widths, widthStart,
725                        ellipsisWidth, ellipsize, j,
726                        textWidth, paint, forceEllipsis);
727            }
728        }
729
730        mLineCount++;
731        return v;
732    }
733
734    private void calculateEllipsis(int lineStart, int lineEnd,
735                                   float[] widths, int widthStart,
736                                   float avail, TextUtils.TruncateAt where,
737                                   int line, float textWidth, TextPaint paint,
738                                   boolean forceEllipsis) {
739        if (textWidth <= avail && !forceEllipsis) {
740            // Everything fits!
741            mLines[mColumns * line + ELLIPSIS_START] = 0;
742            mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
743            return;
744        }
745
746        float ellipsisWidth = paint.measureText(
747                (where == TextUtils.TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL);
748        int ellipsisStart = 0;
749        int ellipsisCount = 0;
750        int len = lineEnd - lineStart;
751
752        // We only support start ellipsis on a single line
753        if (where == TextUtils.TruncateAt.START) {
754            if (mMaximumVisibleLineCount == 1) {
755                float sum = 0;
756                int i;
757
758                for (i = len; i >= 0; i--) {
759                    float w = widths[i - 1 + lineStart - widthStart];
760
761                    if (w + sum + ellipsisWidth > avail) {
762                        break;
763                    }
764
765                    sum += w;
766                }
767
768                ellipsisStart = 0;
769                ellipsisCount = i;
770            } else {
771                if (Log.isLoggable(TAG, Log.WARN)) {
772                    Log.w(TAG, "Start Ellipsis only supported with one line");
773                }
774            }
775        } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
776                where == TextUtils.TruncateAt.END_SMALL) {
777            float sum = 0;
778            int i;
779
780            for (i = 0; i < len; i++) {
781                float w = widths[i + lineStart - widthStart];
782
783                if (w + sum + ellipsisWidth > avail) {
784                    break;
785                }
786
787                sum += w;
788            }
789
790            ellipsisStart = i;
791            ellipsisCount = len - i;
792            if (forceEllipsis && ellipsisCount == 0 && len > 0) {
793                ellipsisStart = len - 1;
794                ellipsisCount = 1;
795            }
796        } else {
797            // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line
798            if (mMaximumVisibleLineCount == 1) {
799                float lsum = 0, rsum = 0;
800                int left = 0, right = len;
801
802                float ravail = (avail - ellipsisWidth) / 2;
803                for (right = len; right >= 0; right--) {
804                    float w = widths[right - 1 + lineStart - widthStart];
805
806                    if (w + rsum > ravail) {
807                        break;
808                    }
809
810                    rsum += w;
811                }
812
813                float lavail = avail - ellipsisWidth - rsum;
814                for (left = 0; left < right; left++) {
815                    float w = widths[left + lineStart - widthStart];
816
817                    if (w + lsum > lavail) {
818                        break;
819                    }
820
821                    lsum += w;
822                }
823
824                ellipsisStart = left;
825                ellipsisCount = right - left;
826            } else {
827                if (Log.isLoggable(TAG, Log.WARN)) {
828                    Log.w(TAG, "Middle Ellipsis only supported with one line");
829                }
830            }
831        }
832
833        mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
834        mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
835    }
836
837    // Override the base class so we can directly access our members,
838    // rather than relying on member functions.
839    // The logic mirrors that of Layout.getLineForVertical
840    // FIXME: It may be faster to do a linear search for layouts without many lines.
841    @Override
842    public int getLineForVertical(int vertical) {
843        int high = mLineCount;
844        int low = -1;
845        int guess;
846        int[] lines = mLines;
847        while (high - low > 1) {
848            guess = (high + low) >> 1;
849            if (lines[mColumns * guess + TOP] > vertical){
850                high = guess;
851            } else {
852                low = guess;
853            }
854        }
855        if (low < 0) {
856            return 0;
857        } else {
858            return low;
859        }
860    }
861
862    @Override
863    public int getLineCount() {
864        return mLineCount;
865    }
866
867    @Override
868    public int getLineTop(int line) {
869        int top = mLines[mColumns * line + TOP];
870        if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount &&
871                line != mLineCount) {
872            top += getBottomPadding();
873        }
874        return top;
875    }
876
877    @Override
878    public int getLineDescent(int line) {
879        int descent = mLines[mColumns * line + DESCENT];
880        if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount - 1 && // -1 intended
881                line != mLineCount) {
882            descent += getBottomPadding();
883        }
884        return descent;
885    }
886
887    @Override
888    public int getLineStart(int line) {
889        return mLines[mColumns * line + START] & START_MASK;
890    }
891
892    @Override
893    public int getParagraphDirection(int line) {
894        return mLines[mColumns * line + DIR] >> DIR_SHIFT;
895    }
896
897    @Override
898    public boolean getLineContainsTab(int line) {
899        return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
900    }
901
902    @Override
903    public final Directions getLineDirections(int line) {
904        return mLineDirections[line];
905    }
906
907    @Override
908    public int getTopPadding() {
909        return mTopPadding;
910    }
911
912    @Override
913    public int getBottomPadding() {
914        return mBottomPadding;
915    }
916
917    @Override
918    public int getEllipsisCount(int line) {
919        if (mColumns < COLUMNS_ELLIPSIZE) {
920            return 0;
921        }
922
923        return mLines[mColumns * line + ELLIPSIS_COUNT];
924    }
925
926    @Override
927    public int getEllipsisStart(int line) {
928        if (mColumns < COLUMNS_ELLIPSIZE) {
929            return 0;
930        }
931
932        return mLines[mColumns * line + ELLIPSIS_START];
933    }
934
935    @Override
936    public int getEllipsizedWidth() {
937        return mEllipsizedWidth;
938    }
939
940    void prepare() {
941        mMeasured = MeasuredText.obtain();
942    }
943
944    void finish() {
945        mMeasured = MeasuredText.recycle(mMeasured);
946    }
947
948    private int mLineCount;
949    private int mTopPadding, mBottomPadding;
950    private int mColumns;
951    private int mEllipsizedWidth;
952
953    private static final int COLUMNS_NORMAL = 3;
954    private static final int COLUMNS_ELLIPSIZE = 5;
955    private static final int START = 0;
956    private static final int DIR = START;
957    private static final int TAB = START;
958    private static final int TOP = 1;
959    private static final int DESCENT = 2;
960    private static final int ELLIPSIS_START = 3;
961    private static final int ELLIPSIS_COUNT = 4;
962
963    private int[] mLines;
964    private Directions[] mLineDirections;
965    private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
966
967    private static final int START_MASK = 0x1FFFFFFF;
968    private static final int DIR_SHIFT  = 30;
969    private static final int TAB_MASK   = 0x20000000;
970
971    private static final int TAB_INCREMENT = 20; // same as Layout, but that's private
972
973    private static final char CHAR_FIRST_CJK = '\u2E80';
974
975    private static final char CHAR_NEW_LINE = '\n';
976    private static final char CHAR_TAB = '\t';
977    private static final char CHAR_SPACE = ' ';
978    private static final char CHAR_DOT = '.';
979    private static final char CHAR_COMMA = ',';
980    private static final char CHAR_COLON = ':';
981    private static final char CHAR_SEMICOLON = ';';
982    private static final char CHAR_SLASH = '/';
983    private static final char CHAR_HYPHEN = '-';
984
985    private static final double EXTRA_ROUNDING = 0.5;
986
987    private static final String ELLIPSIS_NORMAL = "\u2026"; // this is "..."
988    private static final String ELLIPSIS_TWO_DOTS = "\u2025"; // this is ".."
989
990    private static final int CHAR_FIRST_HIGH_SURROGATE = 0xD800;
991    private static final int CHAR_LAST_LOW_SURROGATE = 0xDFFF;
992
993    /*
994     * This is reused across calls to generate()
995     */
996    private MeasuredText mMeasured;
997    private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
998}
999