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