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                                // / 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            paint.getFontMetricsInt(fm);
478
479            v = out(source,
480                    bufEnd, bufEnd, fm.ascent, fm.descent,
481                    fm.top, fm.bottom,
482                    v,
483                    spacingmult, spacingadd, null,
484                    null, fm, false,
485                    needMultiply, null, DEFAULT_DIR, true, bufEnd,
486                    includepad, trackpad, null,
487                    null, bufStart, ellipsize,
488                    ellipsizedWidth, 0, paint, false);
489        }
490    }
491
492    /**
493     * Returns true if the specified character is one of those specified
494     * as being Ideographic (class ID) by the Unicode Line Breaking Algorithm
495     * (http://www.unicode.org/unicode/reports/tr14/), and is therefore OK
496     * to break between a pair of.
497     *
498     * @param includeNonStarters also return true for category NS
499     *                           (non-starters), which can be broken
500     *                           after but not before.
501     */
502    private static final boolean isIdeographic(char c, boolean includeNonStarters) {
503        if (c >= '\u2E80' && c <= '\u2FFF') {
504            return true; // CJK, KANGXI RADICALS, DESCRIPTION SYMBOLS
505        }
506        if (c == '\u3000') {
507            return true; // IDEOGRAPHIC SPACE
508        }
509        if (c >= '\u3040' && c <= '\u309F') {
510            if (!includeNonStarters) {
511                switch (c) {
512                case '\u3041': //  # HIRAGANA LETTER SMALL A
513                case '\u3043': //  # HIRAGANA LETTER SMALL I
514                case '\u3045': //  # HIRAGANA LETTER SMALL U
515                case '\u3047': //  # HIRAGANA LETTER SMALL E
516                case '\u3049': //  # HIRAGANA LETTER SMALL O
517                case '\u3063': //  # HIRAGANA LETTER SMALL TU
518                case '\u3083': //  # HIRAGANA LETTER SMALL YA
519                case '\u3085': //  # HIRAGANA LETTER SMALL YU
520                case '\u3087': //  # HIRAGANA LETTER SMALL YO
521                case '\u308E': //  # HIRAGANA LETTER SMALL WA
522                case '\u3095': //  # HIRAGANA LETTER SMALL KA
523                case '\u3096': //  # HIRAGANA LETTER SMALL KE
524                case '\u309B': //  # KATAKANA-HIRAGANA VOICED SOUND MARK
525                case '\u309C': //  # KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK
526                case '\u309D': //  # HIRAGANA ITERATION MARK
527                case '\u309E': //  # HIRAGANA VOICED ITERATION MARK
528                    return false;
529                }
530            }
531            return true; // Hiragana (except small characters)
532        }
533        if (c >= '\u30A0' && c <= '\u30FF') {
534            if (!includeNonStarters) {
535                switch (c) {
536                case '\u30A0': //  # KATAKANA-HIRAGANA DOUBLE HYPHEN
537                case '\u30A1': //  # KATAKANA LETTER SMALL A
538                case '\u30A3': //  # KATAKANA LETTER SMALL I
539                case '\u30A5': //  # KATAKANA LETTER SMALL U
540                case '\u30A7': //  # KATAKANA LETTER SMALL E
541                case '\u30A9': //  # KATAKANA LETTER SMALL O
542                case '\u30C3': //  # KATAKANA LETTER SMALL TU
543                case '\u30E3': //  # KATAKANA LETTER SMALL YA
544                case '\u30E5': //  # KATAKANA LETTER SMALL YU
545                case '\u30E7': //  # KATAKANA LETTER SMALL YO
546                case '\u30EE': //  # KATAKANA LETTER SMALL WA
547                case '\u30F5': //  # KATAKANA LETTER SMALL KA
548                case '\u30F6': //  # KATAKANA LETTER SMALL KE
549                case '\u30FB': //  # KATAKANA MIDDLE DOT
550                case '\u30FC': //  # KATAKANA-HIRAGANA PROLONGED SOUND MARK
551                case '\u30FD': //  # KATAKANA ITERATION MARK
552                case '\u30FE': //  # KATAKANA VOICED ITERATION MARK
553                    return false;
554                }
555            }
556            return true; // Katakana (except small characters)
557        }
558        if (c >= '\u3400' && c <= '\u4DB5') {
559            return true; // CJK UNIFIED IDEOGRAPHS EXTENSION A
560        }
561        if (c >= '\u4E00' && c <= '\u9FBB') {
562            return true; // CJK UNIFIED IDEOGRAPHS
563        }
564        if (c >= '\uF900' && c <= '\uFAD9') {
565            return true; // CJK COMPATIBILITY IDEOGRAPHS
566        }
567        if (c >= '\uA000' && c <= '\uA48F') {
568            return true; // YI SYLLABLES
569        }
570        if (c >= '\uA490' && c <= '\uA4CF') {
571            return true; // YI RADICALS
572        }
573        if (c >= '\uFE62' && c <= '\uFE66') {
574            return true; // SMALL PLUS SIGN to SMALL EQUALS SIGN
575        }
576        if (c >= '\uFF10' && c <= '\uFF19') {
577            return true; // WIDE DIGITS
578        }
579
580        return false;
581    }
582
583    private int out(CharSequence text, int start, int end,
584                      int above, int below, int top, int bottom, int v,
585                      float spacingmult, float spacingadd,
586                      LineHeightSpan[] chooseHt, int[] chooseHtv,
587                      Paint.FontMetricsInt fm, boolean hasTabOrEmoji,
588                      boolean needMultiply, byte[] chdirs, int dir,
589                      boolean easy, int bufEnd, boolean includePad,
590                      boolean trackPad, char[] chs,
591                      float[] widths, int widthStart, TextUtils.TruncateAt ellipsize,
592                      float ellipsisWidth, float textWidth,
593                      TextPaint paint, boolean moreChars) {
594        int j = mLineCount;
595        int off = j * mColumns;
596        int want = off + mColumns + TOP;
597        int[] lines = mLines;
598
599        if (want >= lines.length) {
600            int nlen = ArrayUtils.idealIntArraySize(want + 1);
601            int[] grow = new int[nlen];
602            System.arraycopy(lines, 0, grow, 0, lines.length);
603            mLines = grow;
604            lines = grow;
605
606            Directions[] grow2 = new Directions[nlen];
607            System.arraycopy(mLineDirections, 0, grow2, 0,
608                             mLineDirections.length);
609            mLineDirections = grow2;
610        }
611
612        if (chooseHt != null) {
613            fm.ascent = above;
614            fm.descent = below;
615            fm.top = top;
616            fm.bottom = bottom;
617
618            for (int i = 0; i < chooseHt.length; i++) {
619                if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
620                    ((LineHeightSpan.WithDensity) chooseHt[i]).
621                        chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
622
623                } else {
624                    chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
625                }
626            }
627
628            above = fm.ascent;
629            below = fm.descent;
630            top = fm.top;
631            bottom = fm.bottom;
632        }
633
634        if (j == 0) {
635            if (trackPad) {
636                mTopPadding = top - above;
637            }
638
639            if (includePad) {
640                above = top;
641            }
642        }
643        if (end == bufEnd) {
644            if (trackPad) {
645                mBottomPadding = bottom - below;
646            }
647
648            if (includePad) {
649                below = bottom;
650            }
651        }
652
653        int extra;
654
655        if (needMultiply) {
656            double ex = (below - above) * (spacingmult - 1) + spacingadd;
657            if (ex >= 0) {
658                extra = (int)(ex + EXTRA_ROUNDING);
659            } else {
660                extra = -(int)(-ex + EXTRA_ROUNDING);
661            }
662        } else {
663            extra = 0;
664        }
665
666        lines[off + START] = start;
667        lines[off + TOP] = v;
668        lines[off + DESCENT] = below + extra;
669
670        v += (below - above) + extra;
671        lines[off + mColumns + START] = end;
672        lines[off + mColumns + TOP] = v;
673
674        if (hasTabOrEmoji)
675            lines[off + TAB] |= TAB_MASK;
676
677        lines[off + DIR] |= dir << DIR_SHIFT;
678        Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT;
679        // easy means all chars < the first RTL, so no emoji, no nothing
680        // XXX a run with no text or all spaces is easy but might be an empty
681        // RTL paragraph.  Make sure easy is false if this is the case.
682        if (easy) {
683            mLineDirections[j] = linedirs;
684        } else {
685            mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs,
686                    start - widthStart, end - start);
687        }
688
689        if (ellipsize != null) {
690            // If there is only one line, then do any type of ellipsis except when it is MARQUEE
691            // if there are multiple lines, just allow END ellipsis on the last line
692            boolean firstLine = (j == 0);
693            boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
694            boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
695
696            boolean doEllipsis =
697                        (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) &&
698                                ellipsize != TextUtils.TruncateAt.MARQUEE) ||
699                        (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
700                                ellipsize == TextUtils.TruncateAt.END);
701            if (doEllipsis) {
702                calculateEllipsis(start, end, widths, widthStart,
703                        ellipsisWidth, ellipsize, j,
704                        textWidth, paint, forceEllipsis);
705            }
706        }
707
708        mLineCount++;
709        return v;
710    }
711
712    private void calculateEllipsis(int lineStart, int lineEnd,
713                                   float[] widths, int widthStart,
714                                   float avail, TextUtils.TruncateAt where,
715                                   int line, float textWidth, TextPaint paint,
716                                   boolean forceEllipsis) {
717        if (textWidth <= avail && !forceEllipsis) {
718            // Everything fits!
719            mLines[mColumns * line + ELLIPSIS_START] = 0;
720            mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
721            return;
722        }
723
724        float ellipsisWidth = paint.measureText(
725                (where == TextUtils.TruncateAt.END_SMALL) ?
726                        ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL, 0, 1);
727        int ellipsisStart = 0;
728        int ellipsisCount = 0;
729        int len = lineEnd - lineStart;
730
731        // We only support start ellipsis on a single line
732        if (where == TextUtils.TruncateAt.START) {
733            if (mMaximumVisibleLineCount == 1) {
734                float sum = 0;
735                int i;
736
737                for (i = len; i >= 0; i--) {
738                    float w = widths[i - 1 + lineStart - widthStart];
739
740                    if (w + sum + ellipsisWidth > avail) {
741                        break;
742                    }
743
744                    sum += w;
745                }
746
747                ellipsisStart = 0;
748                ellipsisCount = i;
749            } else {
750                if (Log.isLoggable(TAG, Log.WARN)) {
751                    Log.w(TAG, "Start Ellipsis only supported with one line");
752                }
753            }
754        } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
755                where == TextUtils.TruncateAt.END_SMALL) {
756            float sum = 0;
757            int i;
758
759            for (i = 0; i < len; i++) {
760                float w = widths[i + lineStart - widthStart];
761
762                if (w + sum + ellipsisWidth > avail) {
763                    break;
764                }
765
766                sum += w;
767            }
768
769            ellipsisStart = i;
770            ellipsisCount = len - i;
771            if (forceEllipsis && ellipsisCount == 0 && len > 0) {
772                ellipsisStart = len - 1;
773                ellipsisCount = 1;
774            }
775        } else {
776            // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line
777            if (mMaximumVisibleLineCount == 1) {
778                float lsum = 0, rsum = 0;
779                int left = 0, right = len;
780
781                float ravail = (avail - ellipsisWidth) / 2;
782                for (right = len; right >= 0; right--) {
783                    float w = widths[right - 1 + lineStart - widthStart];
784
785                    if (w + rsum > ravail) {
786                        break;
787                    }
788
789                    rsum += w;
790                }
791
792                float lavail = avail - ellipsisWidth - rsum;
793                for (left = 0; left < right; left++) {
794                    float w = widths[left + lineStart - widthStart];
795
796                    if (w + lsum > lavail) {
797                        break;
798                    }
799
800                    lsum += w;
801                }
802
803                ellipsisStart = left;
804                ellipsisCount = right - left;
805            } else {
806                if (Log.isLoggable(TAG, Log.WARN)) {
807                    Log.w(TAG, "Middle Ellipsis only supported with one line");
808                }
809            }
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    void prepare() {
920        mMeasured = MeasuredText.obtain();
921    }
922
923    void finish() {
924        mMeasured = MeasuredText.recycle(mMeasured);
925    }
926
927    private int mLineCount;
928    private int mTopPadding, mBottomPadding;
929    private int mColumns;
930    private int mEllipsizedWidth;
931
932    private static final int COLUMNS_NORMAL = 3;
933    private static final int COLUMNS_ELLIPSIZE = 5;
934    private static final int START = 0;
935    private static final int DIR = START;
936    private static final int TAB = START;
937    private static final int TOP = 1;
938    private static final int DESCENT = 2;
939    private static final int ELLIPSIS_START = 3;
940    private static final int ELLIPSIS_COUNT = 4;
941
942    private int[] mLines;
943    private Directions[] mLineDirections;
944    private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
945
946    private static final int START_MASK = 0x1FFFFFFF;
947    private static final int DIR_SHIFT  = 30;
948    private static final int TAB_MASK   = 0x20000000;
949
950    private static final int TAB_INCREMENT = 20; // same as Layout, but that's private
951
952    private static final char CHAR_FIRST_CJK = '\u2E80';
953
954    private static final char CHAR_NEW_LINE = '\n';
955    private static final char CHAR_TAB = '\t';
956    private static final char CHAR_SPACE = ' ';
957    private static final char CHAR_SLASH = '/';
958    private static final char CHAR_HYPHEN = '-';
959
960    private static final double EXTRA_ROUNDING = 0.5;
961
962    private static final int CHAR_FIRST_HIGH_SURROGATE = 0xD800;
963    private static final int CHAR_LAST_LOW_SURROGATE = 0xDFFF;
964
965    /*
966     * This is reused across calls to generate()
967     */
968    private MeasuredText mMeasured;
969    private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
970}
971