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