StaticLayout.java revision d29bdb266d54b4551f42776bb790e80147a279d0
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;
29import com.android.internal.util.GrowingArrayUtils;
30
31/**
32 * StaticLayout is a Layout for text that will not be edited after it
33 * is laid out.  Use {@link DynamicLayout} for text that may change.
34 * <p>This is used by widgets to control text layout. You should not need
35 * to use this class directly unless you are implementing your own widget
36 * or custom display object, or would be tempted to call
37 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int,
38 * float, float, android.graphics.Paint)
39 * Canvas.drawText()} directly.</p>
40 */
41public class StaticLayout extends Layout {
42
43    static final String TAG = "StaticLayout";
44
45    public StaticLayout(CharSequence source, TextPaint paint,
46                        int width,
47                        Alignment align, float spacingmult, float spacingadd,
48                        boolean includepad) {
49        this(source, 0, source.length(), paint, width, align,
50             spacingmult, spacingadd, includepad);
51    }
52
53    /**
54     * @hide
55     */
56    public StaticLayout(CharSequence source, TextPaint paint,
57            int width, Alignment align, TextDirectionHeuristic textDir,
58            float spacingmult, float spacingadd,
59            boolean includepad) {
60        this(source, 0, source.length(), paint, width, align, textDir,
61                spacingmult, spacingadd, includepad);
62    }
63
64    public StaticLayout(CharSequence source, int bufstart, int bufend,
65                        TextPaint paint, int outerwidth,
66                        Alignment align,
67                        float spacingmult, float spacingadd,
68                        boolean includepad) {
69        this(source, bufstart, bufend, paint, outerwidth, align,
70             spacingmult, spacingadd, includepad, null, 0);
71    }
72
73    /**
74     * @hide
75     */
76    public StaticLayout(CharSequence source, int bufstart, int bufend,
77            TextPaint paint, int outerwidth,
78            Alignment align, TextDirectionHeuristic textDir,
79            float spacingmult, float spacingadd,
80            boolean includepad) {
81        this(source, bufstart, bufend, paint, outerwidth, align, textDir,
82                spacingmult, spacingadd, includepad, null, 0, Integer.MAX_VALUE);
83}
84
85    public StaticLayout(CharSequence source, int bufstart, int bufend,
86            TextPaint paint, int outerwidth,
87            Alignment align,
88            float spacingmult, float spacingadd,
89            boolean includepad,
90            TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
91        this(source, bufstart, bufend, paint, outerwidth, align,
92                TextDirectionHeuristics.FIRSTSTRONG_LTR,
93                spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
94    }
95
96    /**
97     * @hide
98     */
99    public StaticLayout(CharSequence source, int bufstart, int bufend,
100                        TextPaint paint, int outerwidth,
101                        Alignment align, TextDirectionHeuristic textDir,
102                        float spacingmult, float spacingadd,
103                        boolean includepad,
104                        TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) {
105        super((ellipsize == null)
106                ? source
107                : (source instanceof Spanned)
108                    ? new SpannedEllipsizer(source)
109                    : new Ellipsizer(source),
110              paint, outerwidth, align, textDir, spacingmult, spacingadd);
111
112        /*
113         * This is annoying, but we can't refer to the layout until
114         * superclass construction is finished, and the superclass
115         * constructor wants the reference to the display text.
116         *
117         * This will break if the superclass constructor ever actually
118         * cares about the content instead of just holding the reference.
119         */
120        if (ellipsize != null) {
121            Ellipsizer e = (Ellipsizer) getText();
122
123            e.mLayout = this;
124            e.mWidth = ellipsizedWidth;
125            e.mMethod = ellipsize;
126            mEllipsizedWidth = ellipsizedWidth;
127
128            mColumns = COLUMNS_ELLIPSIZE;
129        } else {
130            mColumns = COLUMNS_NORMAL;
131            mEllipsizedWidth = outerwidth;
132        }
133
134        mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns);
135        mLines = new int[mLineDirections.length];
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        mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns);
153        mLines = new int[mLineDirections.length];
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        int[] breakOpp = null;
165        final String localeLanguageTag = paint.getTextLocale().toLanguageTag();
166
167        mLineCount = 0;
168
169        int v = 0;
170        boolean needMultiply = (spacingmult != 1 || spacingadd != 0);
171
172        Paint.FontMetricsInt fm = mFontMetricsInt;
173        int[] chooseHtv = null;
174
175        MeasuredText measured = mMeasured;
176
177        Spanned spanned = null;
178        if (source instanceof Spanned)
179            spanned = (Spanned) source;
180
181        int paraEnd;
182        for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
183            paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd);
184            if (paraEnd < 0)
185                paraEnd = bufEnd;
186            else
187                paraEnd++;
188
189            int firstWidthLineLimit = mLineCount + 1;
190            int firstWidth = outerWidth;
191            int restWidth = outerWidth;
192
193            LineHeightSpan[] chooseHt = null;
194
195            if (spanned != null) {
196                LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd,
197                        LeadingMarginSpan.class);
198                for (int i = 0; i < sp.length; i++) {
199                    LeadingMarginSpan lms = sp[i];
200                    firstWidth -= sp[i].getLeadingMargin(true);
201                    restWidth -= sp[i].getLeadingMargin(false);
202
203                    // LeadingMarginSpan2 is odd.  The count affects all
204                    // leading margin spans, not just this particular one
205                    if (lms instanceof LeadingMarginSpan2) {
206                        LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms;
207                        int lmsFirstLine = getLineForOffset(spanned.getSpanStart(lms2));
208                        firstWidthLineLimit = Math.max(firstWidthLineLimit,
209                                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 = ArrayUtils.newUnpaddedIntArray(chooseHt.length);
219                    }
220
221                    for (int i = 0; i < chooseHt.length; i++) {
222                        int o = spanned.getSpanStart(chooseHt[i]);
223
224                        if (o < paraStart) {
225                            // starts in this layout, before the
226                            // current paragraph
227
228                            chooseHtv[i] = getLineTop(getLineForOffset(o));
229                        } else {
230                            // starts in this paragraph
231
232                            chooseHtv[i] = v;
233                        }
234                    }
235                }
236            }
237
238            measured.setPara(source, paraStart, paraEnd, textDir);
239            char[] chs = measured.mChars;
240            float[] widths = measured.mWidths;
241            byte[] chdirs = measured.mLevels;
242            int dir = measured.mDir;
243            boolean easy = measured.mEasy;
244
245            breakOpp = nLineBreakOpportunities(localeLanguageTag, chs, paraEnd - paraStart, breakOpp);
246            int breakOppIndex = 0;
247
248            int width = firstWidth;
249
250            float w = 0;
251            // here is the offset of the starting character of the line we are currently measuring
252            int here = paraStart;
253
254            // ok is a character offset located after a word separator (space, tab, number...) where
255            // we would prefer to cut the current line. Equals to here when no such break was found.
256            int ok = paraStart;
257            float okWidth = w;
258            int okAscent = 0, okDescent = 0, okTop = 0, okBottom = 0;
259
260            // fit is a character offset such that the [here, fit[ range fits in the allowed width.
261            // We will cut the line there if no ok position is found.
262            int fit = paraStart;
263            float fitWidth = w;
264            int fitAscent = 0, fitDescent = 0, fitTop = 0, fitBottom = 0;
265            // same as fitWidth but not including any trailing whitespace
266            float fitWidthGraphing = w;
267
268            boolean hasTabOrEmoji = false;
269            boolean hasTab = false;
270            TabStops tabStops = null;
271
272            for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
273
274                if (spanned == null) {
275                    spanEnd = paraEnd;
276                    int spanLen = spanEnd - spanStart;
277                    measured.addStyleRun(paint, spanLen, fm);
278                } else {
279                    spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
280                            MetricAffectingSpan.class);
281                    int spanLen = spanEnd - spanStart;
282                    MetricAffectingSpan[] spans =
283                            spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
284                    spans = TextUtils.removeEmptySpans(spans, spanned, MetricAffectingSpan.class);
285                    measured.addStyleRun(paint, spans, spanLen, fm);
286                }
287
288                int fmTop = fm.top;
289                int fmBottom = fm.bottom;
290                int fmAscent = fm.ascent;
291                int fmDescent = fm.descent;
292
293                for (int j = spanStart; j < spanEnd; j++) {
294                    char c = chs[j - paraStart];
295
296                    if (c == CHAR_NEW_LINE) {
297                        // intentionally left empty
298                    } else if (c == CHAR_TAB) {
299                        if (hasTab == false) {
300                            hasTab = true;
301                            hasTabOrEmoji = true;
302                            if (spanned != null) {
303                                // First tab this para, check for tabstops
304                                TabStopSpan[] spans = getParagraphSpans(spanned, paraStart,
305                                        paraEnd, TabStopSpan.class);
306                                if (spans.length > 0) {
307                                    tabStops = new TabStops(TAB_INCREMENT, spans);
308                                }
309                            }
310                        }
311                        if (tabStops != null) {
312                            w = tabStops.nextTab(w);
313                        } else {
314                            w = TabStops.nextDefaultStop(w, TAB_INCREMENT);
315                        }
316                    } else if (c >= CHAR_FIRST_HIGH_SURROGATE && c <= CHAR_LAST_LOW_SURROGATE
317                            && j + 1 < spanEnd) {
318                        int emoji = Character.codePointAt(chs, j - paraStart);
319
320                        if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) {
321                            Bitmap bm = EMOJI_FACTORY.getBitmapFromAndroidPua(emoji);
322
323                            if (bm != null) {
324                                Paint whichPaint;
325
326                                if (spanned == null) {
327                                    whichPaint = paint;
328                                } else {
329                                    whichPaint = mWorkPaint;
330                                }
331
332                                float wid = bm.getWidth() * -whichPaint.ascent() / bm.getHeight();
333
334                                w += wid;
335                                hasTabOrEmoji = true;
336                                j++;
337                            } else {
338                                w += widths[j - paraStart];
339                            }
340                        } else {
341                            w += widths[j - paraStart];
342                        }
343                    } else {
344                        w += widths[j - paraStart];
345                    }
346
347                    boolean isSpaceOrTab = c == CHAR_SPACE || c == CHAR_TAB || c == CHAR_ZWSP;
348
349                    if (w <= width || isSpaceOrTab) {
350                        fitWidth = w;
351                        if (!isSpaceOrTab) {
352                            fitWidthGraphing = w;
353                        }
354                        fit = j + 1;
355
356                        if (fmTop < fitTop)
357                            fitTop = fmTop;
358                        if (fmAscent < fitAscent)
359                            fitAscent = fmAscent;
360                        if (fmDescent > fitDescent)
361                            fitDescent = fmDescent;
362                        if (fmBottom > fitBottom)
363                            fitBottom = fmBottom;
364
365                        while (breakOpp[breakOppIndex] != -1
366                                && breakOpp[breakOppIndex] < j - paraStart + 1) {
367                            breakOppIndex++;
368                        }
369                        boolean isLineBreak = breakOppIndex < breakOpp.length &&
370                                breakOpp[breakOppIndex] == j - paraStart + 1;
371
372                        if (isLineBreak) {
373                            okWidth = fitWidthGraphing;
374                            ok = j + 1;
375
376                            if (fitTop < okTop)
377                                okTop = fitTop;
378                            if (fitAscent < okAscent)
379                                okAscent = fitAscent;
380                            if (fitDescent > okDescent)
381                                okDescent = fitDescent;
382                            if (fitBottom > okBottom)
383                                okBottom = fitBottom;
384                        }
385                    } else {
386                        int endPos;
387                        int above, below, top, bottom;
388                        float currentTextWidth;
389
390                        if (ok != here) {
391                            endPos = ok;
392                            above = okAscent;
393                            below = okDescent;
394                            top = okTop;
395                            bottom = okBottom;
396                            currentTextWidth = okWidth;
397                        } else if (fit != here) {
398                            endPos = fit;
399                            above = fitAscent;
400                            below = fitDescent;
401                            top = fitTop;
402                            bottom = fitBottom;
403                            currentTextWidth = fitWidth;
404                        } else {
405                            // must make progress, so take next character
406                            endPos = here + 1;
407                            // but to deal properly with clusters
408                            // take all zero width characters following that
409                            while (endPos < spanEnd && widths[endPos - paraStart] == 0) {
410                                endPos++;
411                            }
412                            above = fmAscent;
413                            below = fmDescent;
414                            top = fmTop;
415                            bottom = fmBottom;
416                            currentTextWidth = widths[here - paraStart];
417                        }
418
419                        int ellipseEnd = endPos;
420                        if (mMaximumVisibleLineCount == 1 && ellipsize == TextUtils.TruncateAt.MIDDLE) {
421                            ellipseEnd = paraEnd;
422                        }
423                        v = out(source, here, ellipseEnd,
424                                above, below, top, bottom,
425                                v, spacingmult, spacingadd, chooseHt,chooseHtv, fm, hasTabOrEmoji,
426                                needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad,
427                                chs, widths, paraStart, ellipsize, ellipsizedWidth,
428                                currentTextWidth, paint, true);
429
430                        here = endPos;
431                        j = here - 1; // restart j-span loop from here, compensating for the j++
432                        ok = fit = here;
433                        w = 0;
434                        fitWidthGraphing = w;
435                        fitAscent = fitDescent = fitTop = fitBottom = 0;
436                        okAscent = okDescent = okTop = okBottom = 0;
437
438                        if (--firstWidthLineLimit <= 0) {
439                            width = restWidth;
440                        }
441
442                        if (here < spanStart) {
443                            // The text was cut before the beginning of the current span range.
444                            // Exit the span loop, and get spanStart to start over from here.
445                            measured.setPos(here);
446                            spanEnd = here;
447                            break;
448                        }
449
450                        if (mLineCount >= mMaximumVisibleLineCount) {
451                            return;
452                        }
453                    }
454                }
455            }
456
457            if (paraEnd != here && mLineCount < mMaximumVisibleLineCount) {
458                if ((fitTop | fitBottom | fitDescent | fitAscent) == 0) {
459                    paint.getFontMetricsInt(fm);
460
461                    fitTop = fm.top;
462                    fitBottom = fm.bottom;
463                    fitAscent = fm.ascent;
464                    fitDescent = fm.descent;
465                }
466
467                // Log.e("text", "output rest " + here + " to " + end);
468
469                v = out(source,
470                        here, paraEnd, fitAscent, fitDescent,
471                        fitTop, fitBottom,
472                        v,
473                        spacingmult, spacingadd, chooseHt,
474                        chooseHtv, fm, hasTabOrEmoji,
475                        needMultiply, chdirs, dir, easy, bufEnd,
476                        includepad, trackpad, chs,
477                        widths, paraStart, ellipsize,
478                        ellipsizedWidth, w, paint, paraEnd != bufEnd);
479            }
480
481            paraStart = paraEnd;
482
483            if (paraEnd == bufEnd)
484                break;
485        }
486
487        if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) &&
488                mLineCount < mMaximumVisibleLineCount) {
489            // Log.e("text", "output last " + bufEnd);
490
491            measured.setPara(source, bufStart, bufEnd, textDir);
492
493            paint.getFontMetricsInt(fm);
494
495            v = out(source,
496                    bufEnd, bufEnd, fm.ascent, fm.descent,
497                    fm.top, fm.bottom,
498                    v,
499                    spacingmult, spacingadd, null,
500                    null, fm, false,
501                    needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd,
502                    includepad, trackpad, null,
503                    null, bufStart, ellipsize,
504                    ellipsizedWidth, 0, paint, false);
505        }
506    }
507
508    private int out(CharSequence text, int start, int end,
509                      int above, int below, int top, int bottom, int v,
510                      float spacingmult, float spacingadd,
511                      LineHeightSpan[] chooseHt, int[] chooseHtv,
512                      Paint.FontMetricsInt fm, boolean hasTabOrEmoji,
513                      boolean needMultiply, byte[] chdirs, int dir,
514                      boolean easy, int bufEnd, boolean includePad,
515                      boolean trackPad, char[] chs,
516                      float[] widths, int widthStart, TextUtils.TruncateAt ellipsize,
517                      float ellipsisWidth, float textWidth,
518                      TextPaint paint, boolean moreChars) {
519        int j = mLineCount;
520        int off = j * mColumns;
521        int want = off + mColumns + TOP;
522        int[] lines = mLines;
523
524        if (want >= lines.length) {
525            Directions[] grow2 = ArrayUtils.newUnpaddedArray(
526                    Directions.class, GrowingArrayUtils.growSize(want));
527            System.arraycopy(mLineDirections, 0, grow2, 0,
528                             mLineDirections.length);
529            mLineDirections = grow2;
530
531            int[] grow = new int[grow2.length];
532            System.arraycopy(lines, 0, grow, 0, lines.length);
533            mLines = grow;
534            lines = grow;
535        }
536
537        if (chooseHt != null) {
538            fm.ascent = above;
539            fm.descent = below;
540            fm.top = top;
541            fm.bottom = bottom;
542
543            for (int i = 0; i < chooseHt.length; i++) {
544                if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
545                    ((LineHeightSpan.WithDensity) chooseHt[i]).
546                        chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
547
548                } else {
549                    chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
550                }
551            }
552
553            above = fm.ascent;
554            below = fm.descent;
555            top = fm.top;
556            bottom = fm.bottom;
557        }
558
559        boolean firstLine = (j == 0);
560        boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
561        boolean lastLine = currentLineIsTheLastVisibleOne || (end == bufEnd);
562
563        if (firstLine) {
564            if (trackPad) {
565                mTopPadding = top - above;
566            }
567
568            if (includePad) {
569                above = top;
570            }
571        }
572
573        int extra;
574
575        if (lastLine) {
576            if (trackPad) {
577                mBottomPadding = bottom - below;
578            }
579
580            if (includePad) {
581                below = bottom;
582            }
583        }
584
585
586        if (needMultiply && !lastLine) {
587            double ex = (below - above) * (spacingmult - 1) + spacingadd;
588            if (ex >= 0) {
589                extra = (int)(ex + EXTRA_ROUNDING);
590            } else {
591                extra = -(int)(-ex + EXTRA_ROUNDING);
592            }
593        } else {
594            extra = 0;
595        }
596
597        lines[off + START] = start;
598        lines[off + TOP] = v;
599        lines[off + DESCENT] = below + extra;
600
601        v += (below - above) + extra;
602        lines[off + mColumns + START] = end;
603        lines[off + mColumns + TOP] = v;
604
605        if (hasTabOrEmoji)
606            lines[off + TAB] |= TAB_MASK;
607
608        lines[off + DIR] |= dir << DIR_SHIFT;
609        Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT;
610        // easy means all chars < the first RTL, so no emoji, no nothing
611        // XXX a run with no text or all spaces is easy but might be an empty
612        // RTL paragraph.  Make sure easy is false if this is the case.
613        if (easy) {
614            mLineDirections[j] = linedirs;
615        } else {
616            mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs,
617                    start - widthStart, end - start);
618        }
619
620        if (ellipsize != null) {
621            // If there is only one line, then do any type of ellipsis except when it is MARQUEE
622            // if there are multiple lines, just allow END ellipsis on the last line
623            boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
624
625            boolean doEllipsis =
626                        (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) &&
627                                ellipsize != TextUtils.TruncateAt.MARQUEE) ||
628                        (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
629                                ellipsize == TextUtils.TruncateAt.END);
630            if (doEllipsis) {
631                calculateEllipsis(start, end, widths, widthStart,
632                        ellipsisWidth, ellipsize, j,
633                        textWidth, paint, forceEllipsis);
634            }
635        }
636
637        mLineCount++;
638        return v;
639    }
640
641    private void calculateEllipsis(int lineStart, int lineEnd,
642                                   float[] widths, int widthStart,
643                                   float avail, TextUtils.TruncateAt where,
644                                   int line, float textWidth, TextPaint paint,
645                                   boolean forceEllipsis) {
646        if (textWidth <= avail && !forceEllipsis) {
647            // Everything fits!
648            mLines[mColumns * line + ELLIPSIS_START] = 0;
649            mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
650            return;
651        }
652
653        float ellipsisWidth = paint.measureText(
654                (where == TextUtils.TruncateAt.END_SMALL) ?
655                        TextUtils.ELLIPSIS_TWO_DOTS : TextUtils.ELLIPSIS_NORMAL, 0, 1);
656        int ellipsisStart = 0;
657        int ellipsisCount = 0;
658        int len = lineEnd - lineStart;
659
660        // We only support start ellipsis on a single line
661        if (where == TextUtils.TruncateAt.START) {
662            if (mMaximumVisibleLineCount == 1) {
663                float sum = 0;
664                int i;
665
666                for (i = len; i >= 0; i--) {
667                    float w = widths[i - 1 + lineStart - widthStart];
668
669                    if (w + sum + ellipsisWidth > avail) {
670                        break;
671                    }
672
673                    sum += w;
674                }
675
676                ellipsisStart = 0;
677                ellipsisCount = i;
678            } else {
679                if (Log.isLoggable(TAG, Log.WARN)) {
680                    Log.w(TAG, "Start Ellipsis only supported with one line");
681                }
682            }
683        } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
684                where == TextUtils.TruncateAt.END_SMALL) {
685            float sum = 0;
686            int i;
687
688            for (i = 0; i < len; i++) {
689                float w = widths[i + lineStart - widthStart];
690
691                if (w + sum + ellipsisWidth > avail) {
692                    break;
693                }
694
695                sum += w;
696            }
697
698            ellipsisStart = i;
699            ellipsisCount = len - i;
700            if (forceEllipsis && ellipsisCount == 0 && len > 0) {
701                ellipsisStart = len - 1;
702                ellipsisCount = 1;
703            }
704        } else {
705            // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line
706            if (mMaximumVisibleLineCount == 1) {
707                float lsum = 0, rsum = 0;
708                int left = 0, right = len;
709
710                float ravail = (avail - ellipsisWidth) / 2;
711                for (right = len; right > 0; right--) {
712                    float w = widths[right - 1 + lineStart - widthStart];
713
714                    if (w + rsum > ravail) {
715                        break;
716                    }
717
718                    rsum += w;
719                }
720
721                float lavail = avail - ellipsisWidth - rsum;
722                for (left = 0; left < right; left++) {
723                    float w = widths[left + lineStart - widthStart];
724
725                    if (w + lsum > lavail) {
726                        break;
727                    }
728
729                    lsum += w;
730                }
731
732                ellipsisStart = left;
733                ellipsisCount = right - left;
734            } else {
735                if (Log.isLoggable(TAG, Log.WARN)) {
736                    Log.w(TAG, "Middle Ellipsis only supported with one line");
737                }
738            }
739        }
740
741        mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
742        mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
743    }
744
745    // Override the base class so we can directly access our members,
746    // rather than relying on member functions.
747    // The logic mirrors that of Layout.getLineForVertical
748    // FIXME: It may be faster to do a linear search for layouts without many lines.
749    @Override
750    public int getLineForVertical(int vertical) {
751        int high = mLineCount;
752        int low = -1;
753        int guess;
754        int[] lines = mLines;
755        while (high - low > 1) {
756            guess = (high + low) >> 1;
757            if (lines[mColumns * guess + TOP] > vertical){
758                high = guess;
759            } else {
760                low = guess;
761            }
762        }
763        if (low < 0) {
764            return 0;
765        } else {
766            return low;
767        }
768    }
769
770    @Override
771    public int getLineCount() {
772        return mLineCount;
773    }
774
775    @Override
776    public int getLineTop(int line) {
777        int top = mLines[mColumns * line + TOP];
778        if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount &&
779                line != mLineCount) {
780            top += getBottomPadding();
781        }
782        return top;
783    }
784
785    @Override
786    public int getLineDescent(int line) {
787        int descent = mLines[mColumns * line + DESCENT];
788        if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount - 1 && // -1 intended
789                line != mLineCount) {
790            descent += getBottomPadding();
791        }
792        return descent;
793    }
794
795    @Override
796    public int getLineStart(int line) {
797        return mLines[mColumns * line + START] & START_MASK;
798    }
799
800    @Override
801    public int getParagraphDirection(int line) {
802        return mLines[mColumns * line + DIR] >> DIR_SHIFT;
803    }
804
805    @Override
806    public boolean getLineContainsTab(int line) {
807        return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
808    }
809
810    @Override
811    public final Directions getLineDirections(int line) {
812        return mLineDirections[line];
813    }
814
815    @Override
816    public int getTopPadding() {
817        return mTopPadding;
818    }
819
820    @Override
821    public int getBottomPadding() {
822        return mBottomPadding;
823    }
824
825    @Override
826    public int getEllipsisCount(int line) {
827        if (mColumns < COLUMNS_ELLIPSIZE) {
828            return 0;
829        }
830
831        return mLines[mColumns * line + ELLIPSIS_COUNT];
832    }
833
834    @Override
835    public int getEllipsisStart(int line) {
836        if (mColumns < COLUMNS_ELLIPSIZE) {
837            return 0;
838        }
839
840        return mLines[mColumns * line + ELLIPSIS_START];
841    }
842
843    @Override
844    public int getEllipsizedWidth() {
845        return mEllipsizedWidth;
846    }
847
848    void prepare() {
849        mMeasured = MeasuredText.obtain();
850    }
851
852    void finish() {
853        mMeasured = MeasuredText.recycle(mMeasured);
854    }
855
856    // returns an array with terminal sentinel value -1 to indicate end
857    // this is so that arrays can be recycled instead of allocating new arrays
858    // every time
859    private static native int[] nLineBreakOpportunities(String locale, char[] text, int length, int[] recycle);
860
861    private int mLineCount;
862    private int mTopPadding, mBottomPadding;
863    private int mColumns;
864    private int mEllipsizedWidth;
865
866    private static final int COLUMNS_NORMAL = 3;
867    private static final int COLUMNS_ELLIPSIZE = 5;
868    private static final int START = 0;
869    private static final int DIR = START;
870    private static final int TAB = START;
871    private static final int TOP = 1;
872    private static final int DESCENT = 2;
873    private static final int ELLIPSIS_START = 3;
874    private static final int ELLIPSIS_COUNT = 4;
875
876    private int[] mLines;
877    private Directions[] mLineDirections;
878    private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
879
880    private static final int START_MASK = 0x1FFFFFFF;
881    private static final int DIR_SHIFT  = 30;
882    private static final int TAB_MASK   = 0x20000000;
883
884    private static final int TAB_INCREMENT = 20; // same as Layout, but that's private
885
886    private static final char CHAR_NEW_LINE = '\n';
887    private static final char CHAR_TAB = '\t';
888    private static final char CHAR_SPACE = ' ';
889    private static final char CHAR_ZWSP = '\u200B';
890
891    private static final double EXTRA_ROUNDING = 0.5;
892
893    private static final int CHAR_FIRST_HIGH_SURROGATE = 0xD800;
894    private static final int CHAR_LAST_LOW_SURROGATE = 0xDFFF;
895
896    /*
897     * This is reused across calls to generate()
898     */
899    private MeasuredText mMeasured;
900    private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
901}
902