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