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