StaticLayout.java revision 936df680166bd0cd6146f1a0c8fa8ad9b807f794
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 || c == CHAR_ZWSP;
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                                // / is class SY and - is class HY, except when followed by a digit
361                                ((c == CHAR_SLASH || c == CHAR_HYPHEN) &&
362                                (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) ||
363                                // Ideographs are class ID: breakpoints when adjacent, except for NS
364                                // (non-starters), which can be broken after but not before
365                                (c >= CHAR_FIRST_CJK && isIdeographic(c, true) &&
366                                j + 1 < spanEnd && isIdeographic(chs[j + 1 - paraStart], false));
367
368                        if (isLineBreak) {
369                            okWidth = w;
370                            ok = j + 1;
371
372                            if (fitTop < okTop)
373                                okTop = fitTop;
374                            if (fitAscent < okAscent)
375                                okAscent = fitAscent;
376                            if (fitDescent > okDescent)
377                                okDescent = fitDescent;
378                            if (fitBottom > okBottom)
379                                okBottom = fitBottom;
380                        }
381                    } else {
382                        final boolean moreChars = (j + 1 < spanEnd);
383                        int endPos;
384                        int above, below, top, bottom;
385                        float currentTextWidth;
386
387                        if (ok != here) {
388                            endPos = ok;
389                            above = okAscent;
390                            below = okDescent;
391                            top = okTop;
392                            bottom = okBottom;
393                            currentTextWidth = okWidth;
394                        } else if (fit != here) {
395                            endPos = fit;
396                            above = fitAscent;
397                            below = fitDescent;
398                            top = fitTop;
399                            bottom = fitBottom;
400                            currentTextWidth = fitWidth;
401                        } else {
402                            endPos = here + 1;
403                            above = fm.ascent;
404                            below = fm.descent;
405                            top = fm.top;
406                            bottom = fm.bottom;
407                            currentTextWidth = widths[here - paraStart];
408                        }
409
410                        v = out(source, here, endPos,
411                                above, below, top, bottom,
412                                v, spacingmult, spacingadd, chooseHt,chooseHtv, fm, hasTabOrEmoji,
413                                needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad,
414                                chs, widths, paraStart, ellipsize, ellipsizedWidth,
415                                currentTextWidth, paint, moreChars);
416
417                        here = endPos;
418                        j = here - 1; // restart j-span loop from here, compensating for the j++
419                        ok = fit = here;
420                        w = 0;
421                        fitAscent = fitDescent = fitTop = fitBottom = 0;
422                        okAscent = okDescent = okTop = okBottom = 0;
423
424                        if (--firstWidthLineLimit <= 0) {
425                            width = restWidth;
426                        }
427
428                        if (here < spanStart) {
429                            // The text was cut before the beginning of the current span range.
430                            // Exit the span loop, and get spanStart to start over from here.
431                            measured.setPos(here);
432                            spanEnd = here;
433                            break;
434                        }
435
436                        if (mLineCount >= mMaximumVisibleLineCount) {
437                            break;
438                        }
439                    }
440                }
441            }
442
443            if (paraEnd != here && mLineCount < mMaximumVisibleLineCount) {
444                if ((fitTop | fitBottom | fitDescent | fitAscent) == 0) {
445                    paint.getFontMetricsInt(fm);
446
447                    fitTop = fm.top;
448                    fitBottom = fm.bottom;
449                    fitAscent = fm.ascent;
450                    fitDescent = fm.descent;
451                }
452
453                // Log.e("text", "output rest " + here + " to " + end);
454
455                v = out(source,
456                        here, paraEnd, fitAscent, fitDescent,
457                        fitTop, fitBottom,
458                        v,
459                        spacingmult, spacingadd, chooseHt,
460                        chooseHtv, fm, hasTabOrEmoji,
461                        needMultiply, chdirs, dir, easy, bufEnd,
462                        includepad, trackpad, chs,
463                        widths, paraStart, ellipsize,
464                        ellipsizedWidth, w, paint, paraEnd != bufEnd);
465            }
466
467            paraStart = paraEnd;
468
469            if (paraEnd == bufEnd)
470                break;
471        }
472
473        if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) &&
474                mLineCount < mMaximumVisibleLineCount) {
475            // Log.e("text", "output last " + bufEnd);
476
477            measured.setPara(source, bufStart, bufEnd, textDir);
478
479            paint.getFontMetricsInt(fm);
480
481            v = out(source,
482                    bufEnd, bufEnd, fm.ascent, fm.descent,
483                    fm.top, fm.bottom,
484                    v,
485                    spacingmult, spacingadd, null,
486                    null, fm, false,
487                    needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd,
488                    includepad, trackpad, null,
489                    null, bufStart, ellipsize,
490                    ellipsizedWidth, 0, paint, false);
491        }
492    }
493
494    /**
495     * Returns true if the specified character is one of those specified
496     * as being Ideographic (class ID) by the Unicode Line Breaking Algorithm
497     * (http://www.unicode.org/unicode/reports/tr14/), and is therefore OK
498     * to break between a pair of.
499     *
500     * @param includeNonStarters also return true for category NS
501     *                           (non-starters), which can be broken
502     *                           after but not before.
503     */
504    private static final boolean isIdeographic(char c, boolean includeNonStarters) {
505        if (c >= '\u2E80' && c <= '\u2FFF') {
506            return true; // CJK, KANGXI RADICALS, DESCRIPTION SYMBOLS
507        }
508        if (c == '\u3000') {
509            return true; // IDEOGRAPHIC SPACE
510        }
511        if (c >= '\u3040' && c <= '\u309F') {
512            if (!includeNonStarters) {
513                switch (c) {
514                case '\u3041': //  # HIRAGANA LETTER SMALL A
515                case '\u3043': //  # HIRAGANA LETTER SMALL I
516                case '\u3045': //  # HIRAGANA LETTER SMALL U
517                case '\u3047': //  # HIRAGANA LETTER SMALL E
518                case '\u3049': //  # HIRAGANA LETTER SMALL O
519                case '\u3063': //  # HIRAGANA LETTER SMALL TU
520                case '\u3083': //  # HIRAGANA LETTER SMALL YA
521                case '\u3085': //  # HIRAGANA LETTER SMALL YU
522                case '\u3087': //  # HIRAGANA LETTER SMALL YO
523                case '\u308E': //  # HIRAGANA LETTER SMALL WA
524                case '\u3095': //  # HIRAGANA LETTER SMALL KA
525                case '\u3096': //  # HIRAGANA LETTER SMALL KE
526                case '\u309B': //  # KATAKANA-HIRAGANA VOICED SOUND MARK
527                case '\u309C': //  # KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK
528                case '\u309D': //  # HIRAGANA ITERATION MARK
529                case '\u309E': //  # HIRAGANA VOICED ITERATION MARK
530                    return false;
531                }
532            }
533            return true; // Hiragana (except small characters)
534        }
535        if (c >= '\u30A0' && c <= '\u30FF') {
536            if (!includeNonStarters) {
537                switch (c) {
538                case '\u30A0': //  # KATAKANA-HIRAGANA DOUBLE HYPHEN
539                case '\u30A1': //  # KATAKANA LETTER SMALL A
540                case '\u30A3': //  # KATAKANA LETTER SMALL I
541                case '\u30A5': //  # KATAKANA LETTER SMALL U
542                case '\u30A7': //  # KATAKANA LETTER SMALL E
543                case '\u30A9': //  # KATAKANA LETTER SMALL O
544                case '\u30C3': //  # KATAKANA LETTER SMALL TU
545                case '\u30E3': //  # KATAKANA LETTER SMALL YA
546                case '\u30E5': //  # KATAKANA LETTER SMALL YU
547                case '\u30E7': //  # KATAKANA LETTER SMALL YO
548                case '\u30EE': //  # KATAKANA LETTER SMALL WA
549                case '\u30F5': //  # KATAKANA LETTER SMALL KA
550                case '\u30F6': //  # KATAKANA LETTER SMALL KE
551                case '\u30FB': //  # KATAKANA MIDDLE DOT
552                case '\u30FC': //  # KATAKANA-HIRAGANA PROLONGED SOUND MARK
553                case '\u30FD': //  # KATAKANA ITERATION MARK
554                case '\u30FE': //  # KATAKANA VOICED ITERATION MARK
555                    return false;
556                }
557            }
558            return true; // Katakana (except small characters)
559        }
560        if (c >= '\u3400' && c <= '\u4DB5') {
561            return true; // CJK UNIFIED IDEOGRAPHS EXTENSION A
562        }
563        if (c >= '\u4E00' && c <= '\u9FBB') {
564            return true; // CJK UNIFIED IDEOGRAPHS
565        }
566        if (c >= '\uF900' && c <= '\uFAD9') {
567            return true; // CJK COMPATIBILITY IDEOGRAPHS
568        }
569        if (c >= '\uA000' && c <= '\uA48F') {
570            return true; // YI SYLLABLES
571        }
572        if (c >= '\uA490' && c <= '\uA4CF') {
573            return true; // YI RADICALS
574        }
575        if (c >= '\uFE62' && c <= '\uFE66') {
576            return true; // SMALL PLUS SIGN to SMALL EQUALS SIGN
577        }
578        if (c >= '\uFF10' && c <= '\uFF19') {
579            return true; // WIDE DIGITS
580        }
581
582        return false;
583    }
584
585    private int out(CharSequence text, int start, int end,
586                      int above, int below, int top, int bottom, int v,
587                      float spacingmult, float spacingadd,
588                      LineHeightSpan[] chooseHt, int[] chooseHtv,
589                      Paint.FontMetricsInt fm, boolean hasTabOrEmoji,
590                      boolean needMultiply, byte[] chdirs, int dir,
591                      boolean easy, int bufEnd, boolean includePad,
592                      boolean trackPad, char[] chs,
593                      float[] widths, int widthStart, TextUtils.TruncateAt ellipsize,
594                      float ellipsisWidth, float textWidth,
595                      TextPaint paint, boolean moreChars) {
596        int j = mLineCount;
597        int off = j * mColumns;
598        int want = off + mColumns + TOP;
599        int[] lines = mLines;
600
601        if (want >= lines.length) {
602            int nlen = ArrayUtils.idealIntArraySize(want + 1);
603            int[] grow = new int[nlen];
604            System.arraycopy(lines, 0, grow, 0, lines.length);
605            mLines = grow;
606            lines = grow;
607
608            Directions[] grow2 = new Directions[nlen];
609            System.arraycopy(mLineDirections, 0, grow2, 0,
610                             mLineDirections.length);
611            mLineDirections = grow2;
612        }
613
614        if (chooseHt != null) {
615            fm.ascent = above;
616            fm.descent = below;
617            fm.top = top;
618            fm.bottom = bottom;
619
620            for (int i = 0; i < chooseHt.length; i++) {
621                if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
622                    ((LineHeightSpan.WithDensity) chooseHt[i]).
623                        chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
624
625                } else {
626                    chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
627                }
628            }
629
630            above = fm.ascent;
631            below = fm.descent;
632            top = fm.top;
633            bottom = fm.bottom;
634        }
635
636        if (j == 0) {
637            if (trackPad) {
638                mTopPadding = top - above;
639            }
640
641            if (includePad) {
642                above = top;
643            }
644        }
645        if (end == bufEnd) {
646            if (trackPad) {
647                mBottomPadding = bottom - below;
648            }
649
650            if (includePad) {
651                below = bottom;
652            }
653        }
654
655        int extra;
656
657        if (needMultiply && end != bufEnd) {
658            double ex = (below - above) * (spacingmult - 1) + spacingadd;
659            if (ex >= 0) {
660                extra = (int)(ex + EXTRA_ROUNDING);
661            } else {
662                extra = -(int)(-ex + EXTRA_ROUNDING);
663            }
664        } else {
665            extra = 0;
666        }
667
668        lines[off + START] = start;
669        lines[off + TOP] = v;
670        lines[off + DESCENT] = below + extra;
671
672        v += (below - above) + extra;
673        lines[off + mColumns + START] = end;
674        lines[off + mColumns + TOP] = v;
675
676        if (hasTabOrEmoji)
677            lines[off + TAB] |= TAB_MASK;
678
679        lines[off + DIR] |= dir << DIR_SHIFT;
680        Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT;
681        // easy means all chars < the first RTL, so no emoji, no nothing
682        // XXX a run with no text or all spaces is easy but might be an empty
683        // RTL paragraph.  Make sure easy is false if this is the case.
684        if (easy) {
685            mLineDirections[j] = linedirs;
686        } else {
687            mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs,
688                    start - widthStart, end - start);
689        }
690
691        if (ellipsize != null) {
692            // If there is only one line, then do any type of ellipsis except when it is MARQUEE
693            // if there are multiple lines, just allow END ellipsis on the last line
694            boolean firstLine = (j == 0);
695            boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
696            boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
697
698            boolean doEllipsis =
699                        (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) &&
700                                ellipsize != TextUtils.TruncateAt.MARQUEE) ||
701                        (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
702                                ellipsize == TextUtils.TruncateAt.END);
703            if (doEllipsis) {
704                calculateEllipsis(start, end, widths, widthStart,
705                        ellipsisWidth, ellipsize, j,
706                        textWidth, paint, forceEllipsis);
707            }
708        }
709
710        mLineCount++;
711        return v;
712    }
713
714    private void calculateEllipsis(int lineStart, int lineEnd,
715                                   float[] widths, int widthStart,
716                                   float avail, TextUtils.TruncateAt where,
717                                   int line, float textWidth, TextPaint paint,
718                                   boolean forceEllipsis) {
719        if (textWidth <= avail && !forceEllipsis) {
720            // Everything fits!
721            mLines[mColumns * line + ELLIPSIS_START] = 0;
722            mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
723            return;
724        }
725
726        float ellipsisWidth = paint.measureText(
727                (where == TextUtils.TruncateAt.END_SMALL) ?
728                        ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL, 0, 1);
729        int ellipsisStart = 0;
730        int ellipsisCount = 0;
731        int len = lineEnd - lineStart;
732
733        // We only support start ellipsis on a single line
734        if (where == TextUtils.TruncateAt.START) {
735            if (mMaximumVisibleLineCount == 1) {
736                float sum = 0;
737                int i;
738
739                for (i = len; i >= 0; i--) {
740                    float w = widths[i - 1 + lineStart - widthStart];
741
742                    if (w + sum + ellipsisWidth > avail) {
743                        break;
744                    }
745
746                    sum += w;
747                }
748
749                ellipsisStart = 0;
750                ellipsisCount = i;
751            } else {
752                if (Log.isLoggable(TAG, Log.WARN)) {
753                    Log.w(TAG, "Start Ellipsis only supported with one line");
754                }
755            }
756        } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
757                where == TextUtils.TruncateAt.END_SMALL) {
758            float sum = 0;
759            int i;
760
761            for (i = 0; i < len; i++) {
762                float w = widths[i + lineStart - widthStart];
763
764                if (w + sum + ellipsisWidth > avail) {
765                    break;
766                }
767
768                sum += w;
769            }
770
771            ellipsisStart = i;
772            ellipsisCount = len - i;
773            if (forceEllipsis && ellipsisCount == 0 && len > 0) {
774                ellipsisStart = len - 1;
775                ellipsisCount = 1;
776            }
777        } else {
778            // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line
779            if (mMaximumVisibleLineCount == 1) {
780                float lsum = 0, rsum = 0;
781                int left = 0, right = len;
782
783                float ravail = (avail - ellipsisWidth) / 2;
784                for (right = len; right >= 0; right--) {
785                    float w = widths[right - 1 + lineStart - widthStart];
786
787                    if (w + rsum > ravail) {
788                        break;
789                    }
790
791                    rsum += w;
792                }
793
794                float lavail = avail - ellipsisWidth - rsum;
795                for (left = 0; left < right; left++) {
796                    float w = widths[left + lineStart - widthStart];
797
798                    if (w + lsum > lavail) {
799                        break;
800                    }
801
802                    lsum += w;
803                }
804
805                ellipsisStart = left;
806                ellipsisCount = right - left;
807            } else {
808                if (Log.isLoggable(TAG, Log.WARN)) {
809                    Log.w(TAG, "Middle Ellipsis only supported with one line");
810                }
811            }
812        }
813
814        mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
815        mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
816    }
817
818    // Override the base class so we can directly access our members,
819    // rather than relying on member functions.
820    // The logic mirrors that of Layout.getLineForVertical
821    // FIXME: It may be faster to do a linear search for layouts without many lines.
822    @Override
823    public int getLineForVertical(int vertical) {
824        int high = mLineCount;
825        int low = -1;
826        int guess;
827        int[] lines = mLines;
828        while (high - low > 1) {
829            guess = (high + low) >> 1;
830            if (lines[mColumns * guess + TOP] > vertical){
831                high = guess;
832            } else {
833                low = guess;
834            }
835        }
836        if (low < 0) {
837            return 0;
838        } else {
839            return low;
840        }
841    }
842
843    @Override
844    public int getLineCount() {
845        return mLineCount;
846    }
847
848    @Override
849    public int getLineTop(int line) {
850        int top = mLines[mColumns * line + TOP];
851        if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount &&
852                line != mLineCount) {
853            top += getBottomPadding();
854        }
855        return top;
856    }
857
858    @Override
859    public int getLineDescent(int line) {
860        int descent = mLines[mColumns * line + DESCENT];
861        if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount - 1 && // -1 intended
862                line != mLineCount) {
863            descent += getBottomPadding();
864        }
865        return descent;
866    }
867
868    @Override
869    public int getLineStart(int line) {
870        return mLines[mColumns * line + START] & START_MASK;
871    }
872
873    @Override
874    public int getParagraphDirection(int line) {
875        return mLines[mColumns * line + DIR] >> DIR_SHIFT;
876    }
877
878    @Override
879    public boolean getLineContainsTab(int line) {
880        return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
881    }
882
883    @Override
884    public final Directions getLineDirections(int line) {
885        return mLineDirections[line];
886    }
887
888    @Override
889    public int getTopPadding() {
890        return mTopPadding;
891    }
892
893    @Override
894    public int getBottomPadding() {
895        return mBottomPadding;
896    }
897
898    @Override
899    public int getEllipsisCount(int line) {
900        if (mColumns < COLUMNS_ELLIPSIZE) {
901            return 0;
902        }
903
904        return mLines[mColumns * line + ELLIPSIS_COUNT];
905    }
906
907    @Override
908    public int getEllipsisStart(int line) {
909        if (mColumns < COLUMNS_ELLIPSIZE) {
910            return 0;
911        }
912
913        return mLines[mColumns * line + ELLIPSIS_START];
914    }
915
916    @Override
917    public int getEllipsizedWidth() {
918        return mEllipsizedWidth;
919    }
920
921    void prepare() {
922        mMeasured = MeasuredText.obtain();
923    }
924
925    void finish() {
926        mMeasured = MeasuredText.recycle(mMeasured);
927    }
928
929    private int mLineCount;
930    private int mTopPadding, mBottomPadding;
931    private int mColumns;
932    private int mEllipsizedWidth;
933
934    private static final int COLUMNS_NORMAL = 3;
935    private static final int COLUMNS_ELLIPSIZE = 5;
936    private static final int START = 0;
937    private static final int DIR = START;
938    private static final int TAB = START;
939    private static final int TOP = 1;
940    private static final int DESCENT = 2;
941    private static final int ELLIPSIS_START = 3;
942    private static final int ELLIPSIS_COUNT = 4;
943
944    private int[] mLines;
945    private Directions[] mLineDirections;
946    private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
947
948    private static final int START_MASK = 0x1FFFFFFF;
949    private static final int DIR_SHIFT  = 30;
950    private static final int TAB_MASK   = 0x20000000;
951
952    private static final int TAB_INCREMENT = 20; // same as Layout, but that's private
953
954    private static final char CHAR_FIRST_CJK = '\u2E80';
955
956    private static final char CHAR_NEW_LINE = '\n';
957    private static final char CHAR_TAB = '\t';
958    private static final char CHAR_SPACE = ' ';
959    private static final char CHAR_SLASH = '/';
960    private static final char CHAR_HYPHEN = '-';
961    private static final char CHAR_ZWSP = '\u200B';
962
963    private static final double EXTRA_ROUNDING = 0.5;
964
965    private static final int CHAR_FIRST_HIGH_SURROGATE = 0xD800;
966    private static final int CHAR_LAST_LOW_SURROGATE = 0xDFFF;
967
968    /*
969     * This is reused across calls to generate()
970     */
971    private MeasuredText mMeasured;
972    private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
973}
974