StaticLayout.java revision 531c30c62b14881aab31a5133920a971b1fbb50e
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.annotation.Nullable;
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;
27import android.util.Pools.SynchronizedPool;
28
29import com.android.internal.util.ArrayUtils;
30import com.android.internal.util.GrowingArrayUtils;
31
32import java.util.Arrays;
33import java.util.Locale;
34
35/**
36 * StaticLayout is a Layout for text that will not be edited after it
37 * is laid out.  Use {@link DynamicLayout} for text that may change.
38 * <p>This is used by widgets to control text layout. You should not need
39 * to use this class directly unless you are implementing your own widget
40 * or custom display object, or would be tempted to call
41 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int,
42 * float, float, android.graphics.Paint)
43 * Canvas.drawText()} directly.</p>
44 */
45public class StaticLayout extends Layout {
46
47    static final String TAG = "StaticLayout";
48
49    /**
50     * Builder for static layouts. The builder is a newer pattern for constructing
51     * StaticLayout objects and should be preferred over the constructors,
52     * particularly to access newer features. To build a static layout, first
53     * call {@link #obtain} with the required arguments (text, paint, and width),
54     * then call setters for optional parameters, and finally {@link #build}
55     * to build the StaticLayout object. Parameters not explicitly set will get
56     * default values.
57     */
58    public final static class Builder {
59        private Builder() {
60            mNativePtr = nNewBuilder();
61        }
62
63        /**
64         * Obtain a builder for constructing StaticLayout objects
65         *
66         * @param source The text to be laid out, optionally with spans
67         * @param start The index of the start of the text
68         * @param end The index + 1 of the end of the text
69         * @param paint The base paint used for layout
70         * @param width The width in pixels
71         * @return a builder object used for constructing the StaticLayout
72         */
73        public static Builder obtain(CharSequence source, int start, int end, TextPaint paint,
74                int width) {
75            Builder b = sPool.acquire();
76            if (b == null) {
77                b = new Builder();
78            }
79
80            // set default initial values
81            b.mText = source;
82            b.mStart = start;
83            b.mEnd = end;
84            b.mPaint = paint;
85            b.mWidth = width;
86            b.mAlignment = Alignment.ALIGN_NORMAL;
87            b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
88            b.mSpacingMult = 1.0f;
89            b.mSpacingAdd = 0.0f;
90            b.mIncludePad = true;
91            b.mEllipsizedWidth = width;
92            b.mEllipsize = null;
93            b.mMaxLines = Integer.MAX_VALUE;
94
95            b.mMeasuredText = MeasuredText.obtain();
96            return b;
97        }
98
99        private static void recycle(Builder b) {
100            b.mPaint = null;
101            b.mText = null;
102            MeasuredText.recycle(b.mMeasuredText);
103            sPool.release(b);
104        }
105
106        // release any expensive state
107        /* package */ void finish() {
108            nFinishBuilder(mNativePtr);
109            mMeasuredText.finish();
110        }
111
112        public Builder setText(CharSequence source) {
113            return setText(source, 0, source.length());
114        }
115
116        /**
117         * Set the text. Only useful when re-using the builder, which is done for
118         * the internal implementation of {@link DynamicLayout} but not as part
119         * of normal {@link StaticLayout} usage.
120         *
121         * @param source The text to be laid out, optionally with spans
122         * @param start The index of the start of the text
123         * @param end The index + 1 of the end of the text
124         * @return this builder, useful for chaining
125         *
126         * @hide
127         */
128        public Builder setText(CharSequence source, int start, int end) {
129            mText = source;
130            mStart = start;
131            mEnd = end;
132            return this;
133        }
134
135        /**
136         * Set the paint. Internal for reuse cases only.
137         *
138         * @param paint The base paint used for layout
139         * @return this builder, useful for chaining
140         *
141         * @hide
142         */
143        public Builder setPaint(TextPaint paint) {
144            mPaint = paint;
145            return this;
146        }
147
148        /**
149         * Set the width. Internal for reuse cases only.
150         *
151         * @param width The width in pixels
152         * @return this builder, useful for chaining
153         *
154         * @hide
155         */
156        public Builder setWidth(int width) {
157            mWidth = width;
158            if (mEllipsize == null) {
159                mEllipsizedWidth = width;
160            }
161            return this;
162        }
163
164        /**
165         * Set the alignment. The default is {@link Layout.Alignment#ALIGN_NORMAL}.
166         *
167         * @param alignment Alignment for the resulting {@link StaticLayout}
168         * @return this builder, useful for chaining
169         */
170        public Builder setAlignment(Alignment alignment) {
171            mAlignment = alignment;
172            return this;
173        }
174
175        /**
176         * Set the text direction heuristic. The text direction heuristic is used to
177         * resolve text direction based per-paragraph based on the input text. The default is
178         * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}.
179         *
180         * @param textDir text direction heuristic for resolving BiDi behavior.
181         * @return this builder, useful for chaining
182         */
183        public Builder setTextDir(TextDirectionHeuristic textDir) {
184            mTextDir = textDir;
185            return this;
186        }
187
188        /**
189         * Set line spacing parameters. The default is 0.0 for {@code spacingAdd}
190         * and 1.0 for {@code spacingMult}.
191         *
192         * @param spacingAdd line spacing add
193         * @param spacingMult line spacing multiplier
194         * @return this builder, useful for chaining
195         * @see android.widget.TextView#setLineSpacing
196         */
197        public Builder setLineSpacing(float spacingAdd, float spacingMult) {
198            mSpacingAdd = spacingAdd;
199            mSpacingMult = spacingMult;
200            return this;
201        }
202
203        /**
204         * Set whether to include extra space beyond font ascent and descent (which is
205         * needed to avoid clipping in some languages, such as Arabic and Kannada). The
206         * default is {@code true}.
207         *
208         * @param includePad whether to include padding
209         * @return this builder, useful for chaining
210         * @see android.widget.TextView#setIncludeFontPadding
211         */
212        public Builder setIncludePad(boolean includePad) {
213            mIncludePad = includePad;
214            return this;
215        }
216
217        /**
218         * Set the width as used for ellipsizing purposes, if it differs from the
219         * normal layout width. The default is the {@code width}
220         * passed to {@link #obtain}.
221         *
222         * @param ellipsizedWidth width used for ellipsizing, in pixels
223         * @return this builder, useful for chaining
224         * @see android.widget.TextView#setEllipsize
225         */
226        public Builder setEllipsizedWidth(int ellipsizedWidth) {
227            mEllipsizedWidth = ellipsizedWidth;
228            return this;
229        }
230
231        /**
232         * Set ellipsizing on the layout. Causes words that are longer than the view
233         * is wide, or exceeding the number of lines (see #setMaxLines) in the case
234         * of {@link android.text.TextUtils.TruncateAt#END} or
235         * {@link android.text.TextUtils.TruncateAt#MARQUEE}, to be ellipsized instead
236         * of broken. The default is
237         * {@code null}, indicating no ellipsis is to be applied.
238         *
239         * @param ellipsize type of ellipsis behavior
240         * @return this builder, useful for chaining
241         * @see android.widget.TextView#setEllipsize
242         */
243        public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) {
244            mEllipsize = ellipsize;
245            return this;
246        }
247
248        /**
249         * Set maximum number of lines. This is particularly useful in the case of
250         * ellipsizing, where it changes the layout of the last line. The default is
251         * unlimited.
252         *
253         * @param maxLines maximum number of lines in the layout
254         * @return this builder, useful for chaining
255         * @see android.widget.TextView#setMaxLines
256         */
257        public Builder setMaxLines(int maxLines) {
258            mMaxLines = maxLines;
259            return this;
260        }
261
262        /**
263         * Set break strategy, useful for selecting high quality or balanced paragraph
264         * layout options. The default is {@link Layout#BREAK_STRATEGY_SIMPLE}.
265         *
266         * @param breakStrategy break strategy for paragraph layout
267         * @return this builder, useful for chaining
268         * @see android.widget.TextView#setBreakStrategy
269         */
270        public Builder setBreakStrategy(@BreakStrategy int breakStrategy) {
271            mBreakStrategy = breakStrategy;
272            return this;
273        }
274
275        /**
276         * Set indents. Arguments are arrays holding an indent amount, one per line, measured in
277         * pixels. For lines past the last element in the array, the last element repeats.
278         *
279         * @param leftIndents array of indent values for left margin, in pixels
280         * @param rightIndents array of indent values for right margin, in pixels
281         * @return this builder, useful for chaining
282         * @see android.widget.TextView#setIndents
283         */
284        public Builder setIndents(int[] leftIndents, int[] rightIndents) {
285            int leftLen = leftIndents == null ? 0 : leftIndents.length;
286            int rightLen = rightIndents == null ? 0 : rightIndents.length;
287            int[] indents = new int[Math.max(leftLen, rightLen)];
288            for (int i = 0; i < indents.length; i++) {
289                int leftMargin = i < leftLen ? leftIndents[i] : 0;
290                int rightMargin = i < rightLen ? rightIndents[i] : 0;
291                indents[i] = leftMargin + rightMargin;
292            }
293            nSetIndents(mNativePtr, indents);
294            return this;
295        }
296
297        /**
298         * Measurement and break iteration is done in native code. The protocol for using
299         * the native code is as follows.
300         *
301         * For each paragraph, do a nSetupParagraph, which sets paragraph text, line width, tab
302         * stops, break strategy (and possibly other parameters in the future).
303         *
304         * Then, for each run within the paragraph:
305         *  - setLocale (this must be done at least for the first run, optional afterwards)
306         *  - one of the following, depending on the type of run:
307         *    + addStyleRun (a text run, to be measured in native code)
308         *    + addMeasuredRun (a run already measured in Java, passed into native code)
309         *    + addReplacementRun (a replacement run, width is given)
310         *
311         * After measurement, nGetWidths() is valid if the widths are needed (eg for ellipsis).
312         * Run nComputeLineBreaks() to obtain line breaks for the paragraph.
313         *
314         * After all paragraphs, call finish() to release expensive buffers.
315         */
316
317        private void setLocale(Locale locale) {
318            if (!locale.equals(mLocale)) {
319                nSetLocale(mNativePtr, locale.toLanguageTag(), Hyphenator.get(locale));
320                mLocale = locale;
321            }
322        }
323
324        /* package */ float addStyleRun(TextPaint paint, int start, int end, boolean isRtl) {
325            return nAddStyleRun(mNativePtr, paint.getNativeInstance(), paint.mNativeTypeface,
326                    start, end, isRtl);
327        }
328
329        /* package */ void addMeasuredRun(int start, int end, float[] widths) {
330            nAddMeasuredRun(mNativePtr, start, end, widths);
331        }
332
333        /* package */ void addReplacementRun(int start, int end, float width) {
334            nAddReplacementRun(mNativePtr, start, end, width);
335        }
336
337        /**
338         * Build the {@link StaticLayout} after options have been set.
339         *
340         * <p>Note: the builder object must not be reused in any way after calling this
341         * method. Setting parameters after calling this method, or calling it a second
342         * time on the same builder object, will likely lead to unexpected results.
343         *
344         * @return the newly constructed {@link StaticLayout} object
345         */
346        public StaticLayout build() {
347            StaticLayout result = new StaticLayout(this);
348            Builder.recycle(this);
349            return result;
350        }
351
352        @Override
353        protected void finalize() throws Throwable {
354            try {
355                nFreeBuilder(mNativePtr);
356            } finally {
357                super.finalize();
358            }
359        }
360
361        /* package */ long mNativePtr;
362
363        CharSequence mText;
364        int mStart;
365        int mEnd;
366        TextPaint mPaint;
367        int mWidth;
368        Alignment mAlignment;
369        TextDirectionHeuristic mTextDir;
370        float mSpacingMult;
371        float mSpacingAdd;
372        boolean mIncludePad;
373        int mEllipsizedWidth;
374        TextUtils.TruncateAt mEllipsize;
375        int mMaxLines;
376        int mBreakStrategy;
377
378        Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
379
380        // This will go away and be subsumed by native builder code
381        MeasuredText mMeasuredText;
382
383        Locale mLocale;
384
385        private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<Builder>(3);
386    }
387
388    public StaticLayout(CharSequence source, TextPaint paint,
389                        int width,
390                        Alignment align, float spacingmult, float spacingadd,
391                        boolean includepad) {
392        this(source, 0, source.length(), paint, width, align,
393             spacingmult, spacingadd, includepad);
394    }
395
396    /**
397     * @hide
398     */
399    public StaticLayout(CharSequence source, TextPaint paint,
400            int width, Alignment align, TextDirectionHeuristic textDir,
401            float spacingmult, float spacingadd,
402            boolean includepad) {
403        this(source, 0, source.length(), paint, width, align, textDir,
404                spacingmult, spacingadd, includepad);
405    }
406
407    public StaticLayout(CharSequence source, int bufstart, int bufend,
408                        TextPaint paint, int outerwidth,
409                        Alignment align,
410                        float spacingmult, float spacingadd,
411                        boolean includepad) {
412        this(source, bufstart, bufend, paint, outerwidth, align,
413             spacingmult, spacingadd, includepad, null, 0);
414    }
415
416    /**
417     * @hide
418     */
419    public StaticLayout(CharSequence source, int bufstart, int bufend,
420            TextPaint paint, int outerwidth,
421            Alignment align, TextDirectionHeuristic textDir,
422            float spacingmult, float spacingadd,
423            boolean includepad) {
424        this(source, bufstart, bufend, paint, outerwidth, align, textDir,
425                spacingmult, spacingadd, includepad, null, 0, Integer.MAX_VALUE);
426}
427
428    public StaticLayout(CharSequence source, int bufstart, int bufend,
429            TextPaint paint, int outerwidth,
430            Alignment align,
431            float spacingmult, float spacingadd,
432            boolean includepad,
433            TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
434        this(source, bufstart, bufend, paint, outerwidth, align,
435                TextDirectionHeuristics.FIRSTSTRONG_LTR,
436                spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
437    }
438
439    /**
440     * @hide
441     */
442    public StaticLayout(CharSequence source, int bufstart, int bufend,
443                        TextPaint paint, int outerwidth,
444                        Alignment align, TextDirectionHeuristic textDir,
445                        float spacingmult, float spacingadd,
446                        boolean includepad,
447                        TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) {
448        super((ellipsize == null)
449                ? source
450                : (source instanceof Spanned)
451                    ? new SpannedEllipsizer(source)
452                    : new Ellipsizer(source),
453              paint, outerwidth, align, textDir, spacingmult, spacingadd);
454
455        Builder b = Builder.obtain(source, bufstart, bufend, paint, outerwidth)
456            .setAlignment(align)
457            .setTextDir(textDir)
458            .setLineSpacing(spacingadd, spacingmult)
459            .setIncludePad(includepad)
460            .setEllipsizedWidth(ellipsizedWidth)
461            .setEllipsize(ellipsize)
462            .setMaxLines(maxLines);
463        /*
464         * This is annoying, but we can't refer to the layout until
465         * superclass construction is finished, and the superclass
466         * constructor wants the reference to the display text.
467         *
468         * This will break if the superclass constructor ever actually
469         * cares about the content instead of just holding the reference.
470         */
471        if (ellipsize != null) {
472            Ellipsizer e = (Ellipsizer) getText();
473
474            e.mLayout = this;
475            e.mWidth = ellipsizedWidth;
476            e.mMethod = ellipsize;
477            mEllipsizedWidth = ellipsizedWidth;
478
479            mColumns = COLUMNS_ELLIPSIZE;
480        } else {
481            mColumns = COLUMNS_NORMAL;
482            mEllipsizedWidth = outerwidth;
483        }
484
485        mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns);
486        mLines = new int[mLineDirections.length];
487        mMaximumVisibleLineCount = maxLines;
488
489        generate(b, b.mIncludePad, b.mIncludePad);
490
491        Builder.recycle(b);
492    }
493
494    /* package */ StaticLayout(CharSequence text) {
495        super(text, null, 0, null, 0, 0);
496
497        mColumns = COLUMNS_ELLIPSIZE;
498        mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns);
499        mLines = new int[mLineDirections.length];
500    }
501
502    private StaticLayout(Builder b) {
503        super((b.mEllipsize == null)
504                ? b.mText
505                : (b.mText instanceof Spanned)
506                    ? new SpannedEllipsizer(b.mText)
507                    : new Ellipsizer(b.mText),
508                b.mPaint, b.mWidth, b.mAlignment, b.mSpacingMult, b.mSpacingAdd);
509
510        if (b.mEllipsize != null) {
511            Ellipsizer e = (Ellipsizer) getText();
512
513            e.mLayout = this;
514            e.mWidth = b.mEllipsizedWidth;
515            e.mMethod = b.mEllipsize;
516            mEllipsizedWidth = b.mEllipsizedWidth;
517
518            mColumns = COLUMNS_ELLIPSIZE;
519        } else {
520            mColumns = COLUMNS_NORMAL;
521            mEllipsizedWidth = b.mWidth;
522        }
523
524        mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns);
525        mLines = new int[mLineDirections.length];
526        mMaximumVisibleLineCount = b.mMaxLines;
527
528        generate(b, b.mIncludePad, b.mIncludePad);
529    }
530
531    /* package */ void generate(Builder b, boolean includepad, boolean trackpad) {
532        CharSequence source = b.mText;
533        int bufStart = b.mStart;
534        int bufEnd = b.mEnd;
535        TextPaint paint = b.mPaint;
536        int outerWidth = b.mWidth;
537        TextDirectionHeuristic textDir = b.mTextDir;
538        float spacingmult = b.mSpacingMult;
539        float spacingadd = b.mSpacingAdd;
540        float ellipsizedWidth = b.mEllipsizedWidth;
541        TextUtils.TruncateAt ellipsize = b.mEllipsize;
542        LineBreaks lineBreaks = new LineBreaks();  // TODO: move to builder to avoid allocation costs
543        // store span end locations
544        int[] spanEndCache = new int[4];
545        // store fontMetrics per span range
546        // must be a multiple of 4 (and > 0) (store top, bottom, ascent, and descent per range)
547        int[] fmCache = new int[4 * 4];
548        b.setLocale(paint.getTextLocale());  // TODO: also respect LocaleSpan within the text
549
550        mLineCount = 0;
551
552        int v = 0;
553        boolean needMultiply = (spacingmult != 1 || spacingadd != 0);
554
555        Paint.FontMetricsInt fm = b.mFontMetricsInt;
556        int[] chooseHtv = null;
557
558        MeasuredText measured = b.mMeasuredText;
559
560        Spanned spanned = null;
561        if (source instanceof Spanned)
562            spanned = (Spanned) source;
563
564        int paraEnd;
565        for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
566            paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd);
567            if (paraEnd < 0)
568                paraEnd = bufEnd;
569            else
570                paraEnd++;
571
572            int firstWidthLineCount = 1;
573            int firstWidth = outerWidth;
574            int restWidth = outerWidth;
575
576            LineHeightSpan[] chooseHt = null;
577
578            if (spanned != null) {
579                LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd,
580                        LeadingMarginSpan.class);
581                for (int i = 0; i < sp.length; i++) {
582                    LeadingMarginSpan lms = sp[i];
583                    firstWidth -= sp[i].getLeadingMargin(true);
584                    restWidth -= sp[i].getLeadingMargin(false);
585
586                    // LeadingMarginSpan2 is odd.  The count affects all
587                    // leading margin spans, not just this particular one
588                    if (lms instanceof LeadingMarginSpan2) {
589                        LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms;
590                        firstWidthLineCount = Math.max(firstWidthLineCount,
591                                lms2.getLeadingMarginLineCount());
592                    }
593                }
594
595                chooseHt = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class);
596
597                if (chooseHt.length != 0) {
598                    if (chooseHtv == null ||
599                        chooseHtv.length < chooseHt.length) {
600                        chooseHtv = ArrayUtils.newUnpaddedIntArray(chooseHt.length);
601                    }
602
603                    for (int i = 0; i < chooseHt.length; i++) {
604                        int o = spanned.getSpanStart(chooseHt[i]);
605
606                        if (o < paraStart) {
607                            // starts in this layout, before the
608                            // current paragraph
609
610                            chooseHtv[i] = getLineTop(getLineForOffset(o));
611                        } else {
612                            // starts in this paragraph
613
614                            chooseHtv[i] = v;
615                        }
616                    }
617                }
618            }
619
620            measured.setPara(source, paraStart, paraEnd, textDir, b);
621            char[] chs = measured.mChars;
622            float[] widths = measured.mWidths;
623            byte[] chdirs = measured.mLevels;
624            int dir = measured.mDir;
625            boolean easy = measured.mEasy;
626
627            // tab stop locations
628            int[] variableTabStops = null;
629            if (spanned != null) {
630                TabStopSpan[] spans = getParagraphSpans(spanned, paraStart,
631                        paraEnd, TabStopSpan.class);
632                if (spans.length > 0) {
633                    int[] stops = new int[spans.length];
634                    for (int i = 0; i < spans.length; i++) {
635                        stops[i] = spans[i].getTabStop();
636                    }
637                    Arrays.sort(stops, 0, stops.length);
638                    variableTabStops = stops;
639                }
640            }
641
642            nSetupParagraph(b.mNativePtr, chs, paraEnd - paraStart,
643                    firstWidth, firstWidthLineCount, restWidth,
644                    variableTabStops, TAB_INCREMENT, b.mBreakStrategy);
645
646            // measurement has to be done before performing line breaking
647            // but we don't want to recompute fontmetrics or span ranges the
648            // second time, so we cache those and then use those stored values
649            int fmCacheCount = 0;
650            int spanEndCacheCount = 0;
651            for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
652                if (fmCacheCount * 4 >= fmCache.length) {
653                    int[] grow = new int[fmCacheCount * 4 * 2];
654                    System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4);
655                    fmCache = grow;
656                }
657
658                if (spanEndCacheCount >= spanEndCache.length) {
659                    int[] grow = new int[spanEndCacheCount * 2];
660                    System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount);
661                    spanEndCache = grow;
662                }
663
664                if (spanned == null) {
665                    spanEnd = paraEnd;
666                    int spanLen = spanEnd - spanStart;
667                    measured.addStyleRun(paint, spanLen, fm);
668                } else {
669                    spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
670                            MetricAffectingSpan.class);
671                    int spanLen = spanEnd - spanStart;
672                    MetricAffectingSpan[] spans =
673                            spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
674                    spans = TextUtils.removeEmptySpans(spans, spanned, MetricAffectingSpan.class);
675                    measured.addStyleRun(paint, spans, spanLen, fm);
676                }
677
678                // the order of storage here (top, bottom, ascent, descent) has to match the code below
679                // where these values are retrieved
680                fmCache[fmCacheCount * 4 + 0] = fm.top;
681                fmCache[fmCacheCount * 4 + 1] = fm.bottom;
682                fmCache[fmCacheCount * 4 + 2] = fm.ascent;
683                fmCache[fmCacheCount * 4 + 3] = fm.descent;
684                fmCacheCount++;
685
686                spanEndCache[spanEndCacheCount] = spanEnd;
687                spanEndCacheCount++;
688            }
689
690            nGetWidths(b.mNativePtr, widths);
691            int breakCount = nComputeLineBreaks(b.mNativePtr, lineBreaks, lineBreaks.breaks,
692                    lineBreaks.widths, lineBreaks.flags, lineBreaks.breaks.length);
693
694            int[] breaks = lineBreaks.breaks;
695            float[] lineWidths = lineBreaks.widths;
696            int[] flags = lineBreaks.flags;
697
698            // here is the offset of the starting character of the line we are currently measuring
699            int here = paraStart;
700
701            int fmTop = 0, fmBottom = 0, fmAscent = 0, fmDescent = 0;
702            int fmCacheIndex = 0;
703            int spanEndCacheIndex = 0;
704            int breakIndex = 0;
705            for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
706                // retrieve end of span
707                spanEnd = spanEndCache[spanEndCacheIndex++];
708
709                // retrieve cached metrics, order matches above
710                fm.top = fmCache[fmCacheIndex * 4 + 0];
711                fm.bottom = fmCache[fmCacheIndex * 4 + 1];
712                fm.ascent = fmCache[fmCacheIndex * 4 + 2];
713                fm.descent = fmCache[fmCacheIndex * 4 + 3];
714                fmCacheIndex++;
715
716                if (fm.top < fmTop) {
717                    fmTop = fm.top;
718                }
719                if (fm.ascent < fmAscent) {
720                    fmAscent = fm.ascent;
721                }
722                if (fm.descent > fmDescent) {
723                    fmDescent = fm.descent;
724                }
725                if (fm.bottom > fmBottom) {
726                    fmBottom = fm.bottom;
727                }
728
729                // skip breaks ending before current span range
730                while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) {
731                    breakIndex++;
732                }
733
734                while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) {
735                    int endPos = paraStart + breaks[breakIndex];
736
737                    boolean moreChars = (endPos < bufEnd);
738
739                    v = out(source, here, endPos,
740                            fmAscent, fmDescent, fmTop, fmBottom,
741                            v, spacingmult, spacingadd, chooseHt,chooseHtv, fm, flags[breakIndex],
742                            needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad,
743                            chs, widths, paraStart, ellipsize, ellipsizedWidth,
744                            lineWidths[breakIndex], paint, moreChars);
745
746                    if (endPos < spanEnd) {
747                        // preserve metrics for current span
748                        fmTop = fm.top;
749                        fmBottom = fm.bottom;
750                        fmAscent = fm.ascent;
751                        fmDescent = fm.descent;
752                    } else {
753                        fmTop = fmBottom = fmAscent = fmDescent = 0;
754                    }
755
756                    here = endPos;
757                    breakIndex++;
758
759                    if (mLineCount >= mMaximumVisibleLineCount) {
760                        return;
761                    }
762                }
763            }
764
765            if (paraEnd == bufEnd)
766                break;
767        }
768
769        if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) &&
770                mLineCount < mMaximumVisibleLineCount) {
771            // Log.e("text", "output last " + bufEnd);
772
773            measured.setPara(source, bufEnd, bufEnd, textDir, b);
774
775            paint.getFontMetricsInt(fm);
776
777            v = out(source,
778                    bufEnd, bufEnd, fm.ascent, fm.descent,
779                    fm.top, fm.bottom,
780                    v,
781                    spacingmult, spacingadd, null,
782                    null, fm, 0,
783                    needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd,
784                    includepad, trackpad, null,
785                    null, bufStart, ellipsize,
786                    ellipsizedWidth, 0, paint, false);
787        }
788    }
789
790    private int out(CharSequence text, int start, int end,
791                      int above, int below, int top, int bottom, int v,
792                      float spacingmult, float spacingadd,
793                      LineHeightSpan[] chooseHt, int[] chooseHtv,
794                      Paint.FontMetricsInt fm, int flags,
795                      boolean needMultiply, byte[] chdirs, int dir,
796                      boolean easy, int bufEnd, boolean includePad,
797                      boolean trackPad, char[] chs,
798                      float[] widths, int widthStart, TextUtils.TruncateAt ellipsize,
799                      float ellipsisWidth, float textWidth,
800                      TextPaint paint, boolean moreChars) {
801        int j = mLineCount;
802        int off = j * mColumns;
803        int want = off + mColumns + TOP;
804        int[] lines = mLines;
805
806        if (want >= lines.length) {
807            Directions[] grow2 = ArrayUtils.newUnpaddedArray(
808                    Directions.class, GrowingArrayUtils.growSize(want));
809            System.arraycopy(mLineDirections, 0, grow2, 0,
810                             mLineDirections.length);
811            mLineDirections = grow2;
812
813            int[] grow = new int[grow2.length];
814            System.arraycopy(lines, 0, grow, 0, lines.length);
815            mLines = grow;
816            lines = grow;
817        }
818
819        if (chooseHt != null) {
820            fm.ascent = above;
821            fm.descent = below;
822            fm.top = top;
823            fm.bottom = bottom;
824
825            for (int i = 0; i < chooseHt.length; i++) {
826                if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
827                    ((LineHeightSpan.WithDensity) chooseHt[i]).
828                        chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
829
830                } else {
831                    chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
832                }
833            }
834
835            above = fm.ascent;
836            below = fm.descent;
837            top = fm.top;
838            bottom = fm.bottom;
839        }
840
841        boolean firstLine = (j == 0);
842        boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
843        boolean lastLine = currentLineIsTheLastVisibleOne || (end == bufEnd);
844
845        if (firstLine) {
846            if (trackPad) {
847                mTopPadding = top - above;
848            }
849
850            if (includePad) {
851                above = top;
852            }
853        }
854
855        int extra;
856
857        if (lastLine) {
858            if (trackPad) {
859                mBottomPadding = bottom - below;
860            }
861
862            if (includePad) {
863                below = bottom;
864            }
865        }
866
867
868        if (needMultiply && !lastLine) {
869            double ex = (below - above) * (spacingmult - 1) + spacingadd;
870            if (ex >= 0) {
871                extra = (int)(ex + EXTRA_ROUNDING);
872            } else {
873                extra = -(int)(-ex + EXTRA_ROUNDING);
874            }
875        } else {
876            extra = 0;
877        }
878
879        lines[off + START] = start;
880        lines[off + TOP] = v;
881        lines[off + DESCENT] = below + extra;
882
883        v += (below - above) + extra;
884        lines[off + mColumns + START] = end;
885        lines[off + mColumns + TOP] = v;
886
887        // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining
888        // one bit for start field
889        lines[off + TAB] |= flags & TAB_MASK;
890        lines[off + HYPHEN] = flags;
891
892        lines[off + DIR] |= dir << DIR_SHIFT;
893        Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT;
894        // easy means all chars < the first RTL, so no emoji, no nothing
895        // XXX a run with no text or all spaces is easy but might be an empty
896        // RTL paragraph.  Make sure easy is false if this is the case.
897        if (easy) {
898            mLineDirections[j] = linedirs;
899        } else {
900            mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs,
901                    start - widthStart, end - start);
902        }
903
904        if (ellipsize != null) {
905            // If there is only one line, then do any type of ellipsis except when it is MARQUEE
906            // if there are multiple lines, just allow END ellipsis on the last line
907            boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
908
909            boolean doEllipsis =
910                        (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) &&
911                                ellipsize != TextUtils.TruncateAt.MARQUEE) ||
912                        (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
913                                ellipsize == TextUtils.TruncateAt.END);
914            if (doEllipsis) {
915                calculateEllipsis(start, end, widths, widthStart,
916                        ellipsisWidth, ellipsize, j,
917                        textWidth, paint, forceEllipsis);
918            }
919        }
920
921        mLineCount++;
922        return v;
923    }
924
925    private void calculateEllipsis(int lineStart, int lineEnd,
926                                   float[] widths, int widthStart,
927                                   float avail, TextUtils.TruncateAt where,
928                                   int line, float textWidth, TextPaint paint,
929                                   boolean forceEllipsis) {
930        if (textWidth <= avail && !forceEllipsis) {
931            // Everything fits!
932            mLines[mColumns * line + ELLIPSIS_START] = 0;
933            mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
934            return;
935        }
936
937        float ellipsisWidth = paint.measureText(
938                (where == TextUtils.TruncateAt.END_SMALL) ?
939                        TextUtils.ELLIPSIS_TWO_DOTS : TextUtils.ELLIPSIS_NORMAL, 0, 1);
940        int ellipsisStart = 0;
941        int ellipsisCount = 0;
942        int len = lineEnd - lineStart;
943
944        // We only support start ellipsis on a single line
945        if (where == TextUtils.TruncateAt.START) {
946            if (mMaximumVisibleLineCount == 1) {
947                float sum = 0;
948                int i;
949
950                for (i = len; i > 0; i--) {
951                    float w = widths[i - 1 + lineStart - widthStart];
952
953                    if (w + sum + ellipsisWidth > avail) {
954                        break;
955                    }
956
957                    sum += w;
958                }
959
960                ellipsisStart = 0;
961                ellipsisCount = i;
962            } else {
963                if (Log.isLoggable(TAG, Log.WARN)) {
964                    Log.w(TAG, "Start Ellipsis only supported with one line");
965                }
966            }
967        } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
968                where == TextUtils.TruncateAt.END_SMALL) {
969            float sum = 0;
970            int i;
971
972            for (i = 0; i < len; i++) {
973                float w = widths[i + lineStart - widthStart];
974
975                if (w + sum + ellipsisWidth > avail) {
976                    break;
977                }
978
979                sum += w;
980            }
981
982            ellipsisStart = i;
983            ellipsisCount = len - i;
984            if (forceEllipsis && ellipsisCount == 0 && len > 0) {
985                ellipsisStart = len - 1;
986                ellipsisCount = 1;
987            }
988        } else {
989            // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line
990            if (mMaximumVisibleLineCount == 1) {
991                float lsum = 0, rsum = 0;
992                int left = 0, right = len;
993
994                float ravail = (avail - ellipsisWidth) / 2;
995                for (right = len; right > 0; right--) {
996                    float w = widths[right - 1 + lineStart - widthStart];
997
998                    if (w + rsum > ravail) {
999                        break;
1000                    }
1001
1002                    rsum += w;
1003                }
1004
1005                float lavail = avail - ellipsisWidth - rsum;
1006                for (left = 0; left < right; left++) {
1007                    float w = widths[left + lineStart - widthStart];
1008
1009                    if (w + lsum > lavail) {
1010                        break;
1011                    }
1012
1013                    lsum += w;
1014                }
1015
1016                ellipsisStart = left;
1017                ellipsisCount = right - left;
1018            } else {
1019                if (Log.isLoggable(TAG, Log.WARN)) {
1020                    Log.w(TAG, "Middle Ellipsis only supported with one line");
1021                }
1022            }
1023        }
1024
1025        mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
1026        mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
1027    }
1028
1029    // Override the base class so we can directly access our members,
1030    // rather than relying on member functions.
1031    // The logic mirrors that of Layout.getLineForVertical
1032    // FIXME: It may be faster to do a linear search for layouts without many lines.
1033    @Override
1034    public int getLineForVertical(int vertical) {
1035        int high = mLineCount;
1036        int low = -1;
1037        int guess;
1038        int[] lines = mLines;
1039        while (high - low > 1) {
1040            guess = (high + low) >> 1;
1041            if (lines[mColumns * guess + TOP] > vertical){
1042                high = guess;
1043            } else {
1044                low = guess;
1045            }
1046        }
1047        if (low < 0) {
1048            return 0;
1049        } else {
1050            return low;
1051        }
1052    }
1053
1054    @Override
1055    public int getLineCount() {
1056        return mLineCount;
1057    }
1058
1059    @Override
1060    public int getLineTop(int line) {
1061        int top = mLines[mColumns * line + TOP];
1062        if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount &&
1063                line != mLineCount) {
1064            top += getBottomPadding();
1065        }
1066        return top;
1067    }
1068
1069    @Override
1070    public int getLineDescent(int line) {
1071        int descent = mLines[mColumns * line + DESCENT];
1072        if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount - 1 && // -1 intended
1073                line != mLineCount) {
1074            descent += getBottomPadding();
1075        }
1076        return descent;
1077    }
1078
1079    @Override
1080    public int getLineStart(int line) {
1081        return mLines[mColumns * line + START] & START_MASK;
1082    }
1083
1084    @Override
1085    public int getParagraphDirection(int line) {
1086        return mLines[mColumns * line + DIR] >> DIR_SHIFT;
1087    }
1088
1089    @Override
1090    public boolean getLineContainsTab(int line) {
1091        return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
1092    }
1093
1094    @Override
1095    public final Directions getLineDirections(int line) {
1096        return mLineDirections[line];
1097    }
1098
1099    @Override
1100    public int getTopPadding() {
1101        return mTopPadding;
1102    }
1103
1104    @Override
1105    public int getBottomPadding() {
1106        return mBottomPadding;
1107    }
1108
1109    /**
1110     * @hide
1111     */
1112    @Override
1113    public int getHyphen(int line) {
1114        return mLines[mColumns * line + HYPHEN] & 0xff;
1115    }
1116
1117    @Override
1118    public int getEllipsisCount(int line) {
1119        if (mColumns < COLUMNS_ELLIPSIZE) {
1120            return 0;
1121        }
1122
1123        return mLines[mColumns * line + ELLIPSIS_COUNT];
1124    }
1125
1126    @Override
1127    public int getEllipsisStart(int line) {
1128        if (mColumns < COLUMNS_ELLIPSIZE) {
1129            return 0;
1130        }
1131
1132        return mLines[mColumns * line + ELLIPSIS_START];
1133    }
1134
1135    @Override
1136    public int getEllipsizedWidth() {
1137        return mEllipsizedWidth;
1138    }
1139
1140    private static native long nNewBuilder();
1141    private static native void nFreeBuilder(long nativePtr);
1142    private static native void nFinishBuilder(long nativePtr);
1143
1144    /* package */ static native long nLoadHyphenator(String patternData);
1145
1146    private static native void nSetLocale(long nativePtr, String locale, long nativeHyphenator);
1147
1148    private static native void nSetIndents(long nativePtr, int[] indents);
1149
1150    // Set up paragraph text and settings; done as one big method to minimize jni crossings
1151    private static native void nSetupParagraph(long nativePtr, char[] text, int length,
1152            float firstWidth, int firstWidthLineCount, float restWidth,
1153            int[] variableTabStops, int defaultTabStop, int breakStrategy);
1154
1155    private static native float nAddStyleRun(long nativePtr, long nativePaint,
1156            long nativeTypeface, int start, int end, boolean isRtl);
1157
1158    private static native void nAddMeasuredRun(long nativePtr,
1159            int start, int end, float[] widths);
1160
1161    private static native void nAddReplacementRun(long nativePtr, int start, int end, float width);
1162
1163    private static native void nGetWidths(long nativePtr, float[] widths);
1164
1165    // populates LineBreaks and returns the number of breaks found
1166    //
1167    // the arrays inside the LineBreaks objects are passed in as well
1168    // to reduce the number of JNI calls in the common case where the
1169    // arrays do not have to be resized
1170    private static native int nComputeLineBreaks(long nativePtr, LineBreaks recycle,
1171            int[] recycleBreaks, float[] recycleWidths, int[] recycleFlags, int recycleLength);
1172
1173    private int mLineCount;
1174    private int mTopPadding, mBottomPadding;
1175    private int mColumns;
1176    private int mEllipsizedWidth;
1177
1178    private static final int COLUMNS_NORMAL = 4;
1179    private static final int COLUMNS_ELLIPSIZE = 6;
1180    private static final int START = 0;
1181    private static final int DIR = START;
1182    private static final int TAB = START;
1183    private static final int TOP = 1;
1184    private static final int DESCENT = 2;
1185    private static final int HYPHEN = 3;
1186    private static final int ELLIPSIS_START = 4;
1187    private static final int ELLIPSIS_COUNT = 5;
1188
1189    private int[] mLines;
1190    private Directions[] mLineDirections;
1191    private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
1192
1193    private static final int START_MASK = 0x1FFFFFFF;
1194    private static final int DIR_SHIFT  = 30;
1195    private static final int TAB_MASK   = 0x20000000;
1196
1197    private static final int TAB_INCREMENT = 20; // same as Layout, but that's private
1198
1199    private static final char CHAR_NEW_LINE = '\n';
1200
1201    private static final double EXTRA_ROUNDING = 0.5;
1202
1203    // This is used to return three arrays from a single JNI call when
1204    // performing line breaking
1205    /*package*/ static class LineBreaks {
1206        private static final int INITIAL_SIZE = 16;
1207        public int[] breaks = new int[INITIAL_SIZE];
1208        public float[] widths = new float[INITIAL_SIZE];
1209        public int[] flags = new int[INITIAL_SIZE]; // hasTabOrEmoji
1210        // breaks, widths, and flags should all have the same length
1211    }
1212
1213}
1214