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