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 
17 package android.text;
18 
19 import android.annotation.Nullable;
20 import android.graphics.Paint;
21 import android.text.style.LeadingMarginSpan;
22 import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
23 import android.text.style.LineHeightSpan;
24 import android.text.style.MetricAffectingSpan;
25 import android.text.style.TabStopSpan;
26 import android.util.Log;
27 import android.util.Pools.SynchronizedPool;
28 
29 import com.android.internal.util.ArrayUtils;
30 import com.android.internal.util.GrowingArrayUtils;
31 
32 import java.util.Arrays;
33 import 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  */
45 public 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 {
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          */
obtain(CharSequence source, int start, int end, TextPaint paint, int width)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 
recycle(Builder b)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
finish()113         /* package */ void finish() {
114             nFinishBuilder(mNativePtr);
115             mText = null;
116             mPaint = null;
117             mLeftIndents = null;
118             mRightIndents = null;
119             mMeasuredText.finish();
120         }
121 
setText(CharSequence source)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          */
setText(CharSequence source, int start, int end)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          */
setPaint(TextPaint paint)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          */
setWidth(int width)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          */
setAlignment(Alignment alignment)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          */
setTextDirection(TextDirectionHeuristic textDir)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          */
setLineSpacing(float spacingAdd, float spacingMult)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          */
setIncludePad(boolean includePad)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          */
setEllipsizedWidth(int ellipsizedWidth)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          */
setEllipsize(@ullable TextUtils.TruncateAt ellipsize)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          */
setMaxLines(int maxLines)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          */
setBreakStrategy(@reakStrategy int breakStrategy)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          */
setHyphenationFrequency(@yphenationFrequency int hyphenationFrequency)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          */
setIndents(int[] leftIndents, int[] rightIndents)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