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.IntDef;
20 import android.graphics.Canvas;
21 import android.graphics.Paint;
22 import android.graphics.Path;
23 import android.graphics.Rect;
24 import android.text.method.TextKeyListener;
25 import android.text.style.AlignmentSpan;
26 import android.text.style.LeadingMarginSpan;
27 import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
28 import android.text.style.LineBackgroundSpan;
29 import android.text.style.ParagraphStyle;
30 import android.text.style.ReplacementSpan;
31 import android.text.style.TabStopSpan;
32 
33 import com.android.internal.util.ArrayUtils;
34 import com.android.internal.util.GrowingArrayUtils;
35 
36 import java.lang.annotation.Retention;
37 import java.lang.annotation.RetentionPolicy;
38 import java.util.Arrays;
39 
40 /**
41  * A base class that manages text layout in visual elements on
42  * the screen.
43  * <p>For text that will be edited, use a {@link DynamicLayout},
44  * which will be updated as the text changes.
45  * For text that will not change, use a {@link StaticLayout}.
46  */
47 public abstract class Layout {
48     /** @hide */
49     @IntDef({BREAK_STRATEGY_SIMPLE, BREAK_STRATEGY_HIGH_QUALITY, BREAK_STRATEGY_BALANCED})
50     @Retention(RetentionPolicy.SOURCE)
51     public @interface BreakStrategy {}
52 
53     /**
54      * Value for break strategy indicating simple line breaking. Automatic hyphens are not added
55      * (though soft hyphens are respected), and modifying text generally doesn't affect the layout
56      * before it (which yields a more consistent user experience when editing), but layout may not
57      * be the highest quality.
58      */
59     public static final int BREAK_STRATEGY_SIMPLE = 0;
60 
61     /**
62      * Value for break strategy indicating high quality line breaking, including automatic
63      * hyphenation and doing whole-paragraph optimization of line breaks.
64      */
65     public static final int BREAK_STRATEGY_HIGH_QUALITY = 1;
66 
67     /**
68      * Value for break strategy indicating balanced line breaking. The breaks are chosen to
69      * make all lines as close to the same length as possible, including automatic hyphenation.
70      */
71     public static final int BREAK_STRATEGY_BALANCED = 2;
72 
73     /** @hide */
74     @IntDef({HYPHENATION_FREQUENCY_NORMAL, HYPHENATION_FREQUENCY_FULL,
75              HYPHENATION_FREQUENCY_NONE})
76     @Retention(RetentionPolicy.SOURCE)
77     public @interface HyphenationFrequency {}
78 
79     /**
80      * Value for hyphenation frequency indicating no automatic hyphenation. Useful
81      * for backward compatibility, and for cases where the automatic hyphenation algorithm results
82      * in incorrect hyphenation. Mid-word breaks may still happen when a word is wider than the
83      * layout and there is otherwise no valid break. Soft hyphens are ignored and will not be used
84      * as suggestions for potential line breaks.
85      */
86     public static final int HYPHENATION_FREQUENCY_NONE = 0;
87 
88     /**
89      * Value for hyphenation frequency indicating a light amount of automatic hyphenation, which
90      * is a conservative default. Useful for informal cases, such as short sentences or chat
91      * messages.
92      */
93     public static final int HYPHENATION_FREQUENCY_NORMAL = 1;
94 
95     /**
96      * Value for hyphenation frequency indicating the full amount of automatic hyphenation, typical
97      * in typography. Useful for running text and where it's important to put the maximum amount of
98      * text in a screen with limited space.
99      */
100     public static final int HYPHENATION_FREQUENCY_FULL = 2;
101 
102     private static final ParagraphStyle[] NO_PARA_SPANS =
103         ArrayUtils.emptyArray(ParagraphStyle.class);
104 
105     /**
106      * Return how wide a layout must be in order to display the
107      * specified text with one line per paragraph.
108      */
getDesiredWidth(CharSequence source, TextPaint paint)109     public static float getDesiredWidth(CharSequence source,
110                                         TextPaint paint) {
111         return getDesiredWidth(source, 0, source.length(), paint);
112     }
113 
114     /**
115      * Return how wide a layout must be in order to display the
116      * specified text slice with one line per paragraph.
117      */
getDesiredWidth(CharSequence source, int start, int end, TextPaint paint)118     public static float getDesiredWidth(CharSequence source,
119                                         int start, int end,
120                                         TextPaint paint) {
121         float need = 0;
122 
123         int next;
124         for (int i = start; i <= end; i = next) {
125             next = TextUtils.indexOf(source, '\n', i, end);
126 
127             if (next < 0)
128                 next = end;
129 
130             // note, omits trailing paragraph char
131             float w = measurePara(paint, source, i, next);
132 
133             if (w > need)
134                 need = w;
135 
136             next++;
137         }
138 
139         return need;
140     }
141 
142     /**
143      * Subclasses of Layout use this constructor to set the display text,
144      * width, and other standard properties.
145      * @param text the text to render
146      * @param paint the default paint for the layout.  Styles can override
147      * various attributes of the paint.
148      * @param width the wrapping width for the text.
149      * @param align whether to left, right, or center the text.  Styles can
150      * override the alignment.
151      * @param spacingMult factor by which to scale the font size to get the
152      * default line spacing
153      * @param spacingAdd amount to add to the default line spacing
154      */
Layout(CharSequence text, TextPaint paint, int width, Alignment align, float spacingMult, float spacingAdd)155     protected Layout(CharSequence text, TextPaint paint,
156                      int width, Alignment align,
157                      float spacingMult, float spacingAdd) {
158         this(text, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
159                 spacingMult, spacingAdd);
160     }
161 
162     /**
163      * Subclasses of Layout use this constructor to set the display text,
164      * width, and other standard properties.
165      * @param text the text to render
166      * @param paint the default paint for the layout.  Styles can override
167      * various attributes of the paint.
168      * @param width the wrapping width for the text.
169      * @param align whether to left, right, or center the text.  Styles can
170      * override the alignment.
171      * @param spacingMult factor by which to scale the font size to get the
172      * default line spacing
173      * @param spacingAdd amount to add to the default line spacing
174      *
175      * @hide
176      */
Layout(CharSequence text, TextPaint paint, int width, Alignment align, TextDirectionHeuristic textDir, float spacingMult, float spacingAdd)177     protected Layout(CharSequence text, TextPaint paint,
178                      int width, Alignment align, TextDirectionHeuristic textDir,
179                      float spacingMult, float spacingAdd) {
180 
181         if (width < 0)
182             throw new IllegalArgumentException("Layout: " + width + " < 0");
183 
184         // Ensure paint doesn't have baselineShift set.
185         // While normally we don't modify the paint the user passed in,
186         // we were already doing this in Styled.drawUniformRun with both
187         // baselineShift and bgColor.  We probably should reevaluate bgColor.
188         if (paint != null) {
189             paint.bgColor = 0;
190             paint.baselineShift = 0;
191         }
192 
193         mText = text;
194         mPaint = paint;
195         mWidth = width;
196         mAlignment = align;
197         mSpacingMult = spacingMult;
198         mSpacingAdd = spacingAdd;
199         mSpannedText = text instanceof Spanned;
200         mTextDir = textDir;
201     }
202 
203     /**
204      * Replace constructor properties of this Layout with new ones.  Be careful.
205      */
replaceWith(CharSequence text, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd)206     /* package */ void replaceWith(CharSequence text, TextPaint paint,
207                               int width, Alignment align,
208                               float spacingmult, float spacingadd) {
209         if (width < 0) {
210             throw new IllegalArgumentException("Layout: " + width + " < 0");
211         }
212 
213         mText = text;
214         mPaint = paint;
215         mWidth = width;
216         mAlignment = align;
217         mSpacingMult = spacingmult;
218         mSpacingAdd = spacingadd;
219         mSpannedText = text instanceof Spanned;
220     }
221 
222     /**
223      * Draw this Layout on the specified Canvas.
224      */
draw(Canvas c)225     public void draw(Canvas c) {
226         draw(c, null, null, 0);
227     }
228 
229     /**
230      * Draw this Layout on the specified canvas, with the highlight path drawn
231      * between the background and the text.
232      *
233      * @param canvas the canvas
234      * @param highlight the path of the highlight or cursor; can be null
235      * @param highlightPaint the paint for the highlight
236      * @param cursorOffsetVertical the amount to temporarily translate the
237      *        canvas while rendering the highlight
238      */
draw(Canvas canvas, Path highlight, Paint highlightPaint, int cursorOffsetVertical)239     public void draw(Canvas canvas, Path highlight, Paint highlightPaint,
240             int cursorOffsetVertical) {
241         final long lineRange = getLineRangeForDraw(canvas);
242         int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
243         int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
244         if (lastLine < 0) return;
245 
246         drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
247                 firstLine, lastLine);
248         drawText(canvas, firstLine, lastLine);
249     }
250 
251     /**
252      * @hide
253      */
drawText(Canvas canvas, int firstLine, int lastLine)254     public void drawText(Canvas canvas, int firstLine, int lastLine) {
255         int previousLineBottom = getLineTop(firstLine);
256         int previousLineEnd = getLineStart(firstLine);
257         ParagraphStyle[] spans = NO_PARA_SPANS;
258         int spanEnd = 0;
259         TextPaint paint = mPaint;
260         CharSequence buf = mText;
261 
262         Alignment paraAlign = mAlignment;
263         TabStops tabStops = null;
264         boolean tabStopsIsInitialized = false;
265 
266         TextLine tl = TextLine.obtain();
267 
268         // Draw the lines, one at a time.
269         // The baseline is the top of the following line minus the current line's descent.
270         for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {
271             int start = previousLineEnd;
272             previousLineEnd = getLineStart(lineNum + 1);
273             int end = getLineVisibleEnd(lineNum, start, previousLineEnd);
274 
275             int ltop = previousLineBottom;
276             int lbottom = getLineTop(lineNum + 1);
277             previousLineBottom = lbottom;
278             int lbaseline = lbottom - getLineDescent(lineNum);
279 
280             int dir = getParagraphDirection(lineNum);
281             int left = 0;
282             int right = mWidth;
283 
284             if (mSpannedText) {
285                 Spanned sp = (Spanned) buf;
286                 int textLength = buf.length();
287                 boolean isFirstParaLine = (start == 0 || buf.charAt(start - 1) == '\n');
288 
289                 // New batch of paragraph styles, collect into spans array.
290                 // Compute the alignment, last alignment style wins.
291                 // Reset tabStops, we'll rebuild if we encounter a line with
292                 // tabs.
293                 // We expect paragraph spans to be relatively infrequent, use
294                 // spanEnd so that we can check less frequently.  Since
295                 // paragraph styles ought to apply to entire paragraphs, we can
296                 // just collect the ones present at the start of the paragraph.
297                 // If spanEnd is before the end of the paragraph, that's not
298                 // our problem.
299                 if (start >= spanEnd && (lineNum == firstLine || isFirstParaLine)) {
300                     spanEnd = sp.nextSpanTransition(start, textLength,
301                                                     ParagraphStyle.class);
302                     spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);
303 
304                     paraAlign = mAlignment;
305                     for (int n = spans.length - 1; n >= 0; n--) {
306                         if (spans[n] instanceof AlignmentSpan) {
307                             paraAlign = ((AlignmentSpan) spans[n]).getAlignment();
308                             break;
309                         }
310                     }
311 
312                     tabStopsIsInitialized = false;
313                 }
314 
315                 // Draw all leading margin spans.  Adjust left or right according
316                 // to the paragraph direction of the line.
317                 final int length = spans.length;
318                 boolean useFirstLineMargin = isFirstParaLine;
319                 for (int n = 0; n < length; n++) {
320                     if (spans[n] instanceof LeadingMarginSpan2) {
321                         int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount();
322                         int startLine = getLineForOffset(sp.getSpanStart(spans[n]));
323                         // if there is more than one LeadingMarginSpan2, use
324                         // the count that is greatest
325                         if (lineNum < startLine + count) {
326                             useFirstLineMargin = true;
327                             break;
328                         }
329                     }
330                 }
331                 for (int n = 0; n < length; n++) {
332                     if (spans[n] instanceof LeadingMarginSpan) {
333                         LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
334                         if (dir == DIR_RIGHT_TO_LEFT) {
335                             margin.drawLeadingMargin(canvas, paint, right, dir, ltop,
336                                                      lbaseline, lbottom, buf,
337                                                      start, end, isFirstParaLine, this);
338                             right -= margin.getLeadingMargin(useFirstLineMargin);
339                         } else {
340                             margin.drawLeadingMargin(canvas, paint, left, dir, ltop,
341                                                      lbaseline, lbottom, buf,
342                                                      start, end, isFirstParaLine, this);
343                             left += margin.getLeadingMargin(useFirstLineMargin);
344                         }
345                     }
346                 }
347             }
348 
349             boolean hasTab = getLineContainsTab(lineNum);
350             // Can't tell if we have tabs for sure, currently
351             if (hasTab && !tabStopsIsInitialized) {
352                 if (tabStops == null) {
353                     tabStops = new TabStops(TAB_INCREMENT, spans);
354                 } else {
355                     tabStops.reset(TAB_INCREMENT, spans);
356                 }
357                 tabStopsIsInitialized = true;
358             }
359 
360             // Determine whether the line aligns to normal, opposite, or center.
361             Alignment align = paraAlign;
362             if (align == Alignment.ALIGN_LEFT) {
363                 align = (dir == DIR_LEFT_TO_RIGHT) ?
364                     Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
365             } else if (align == Alignment.ALIGN_RIGHT) {
366                 align = (dir == DIR_LEFT_TO_RIGHT) ?
367                     Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
368             }
369 
370             int x;
371             if (align == Alignment.ALIGN_NORMAL) {
372                 if (dir == DIR_LEFT_TO_RIGHT) {
373                     x = left + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
374                 } else {
375                     x = right + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
376                 }
377             } else {
378                 int max = (int)getLineExtent(lineNum, tabStops, false);
379                 if (align == Alignment.ALIGN_OPPOSITE) {
380                     if (dir == DIR_LEFT_TO_RIGHT) {
381                         x = right - max + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
382                     } else {
383                         x = left - max + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
384                     }
385                 } else { // Alignment.ALIGN_CENTER
386                     max = max & ~1;
387                     x = ((right + left - max) >> 1) +
388                             getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
389                 }
390             }
391 
392             paint.setHyphenEdit(getHyphen(lineNum));
393             Directions directions = getLineDirections(lineNum);
394             if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab) {
395                 // XXX: assumes there's nothing additional to be done
396                 canvas.drawText(buf, start, end, x, lbaseline, paint);
397             } else {
398                 tl.set(paint, buf, start, end, dir, directions, hasTab, tabStops);
399                 tl.draw(canvas, x, ltop, lbaseline, lbottom);
400             }
401             paint.setHyphenEdit(0);
402         }
403 
404         TextLine.recycle(tl);
405     }
406 
407     /**
408      * @hide
409      */
drawBackground(Canvas canvas, Path highlight, Paint highlightPaint, int cursorOffsetVertical, int firstLine, int lastLine)410     public void drawBackground(Canvas canvas, Path highlight, Paint highlightPaint,
411             int cursorOffsetVertical, int firstLine, int lastLine) {
412         // First, draw LineBackgroundSpans.
413         // LineBackgroundSpans know nothing about the alignment, margins, or
414         // direction of the layout or line.  XXX: Should they?
415         // They are evaluated at each line.
416         if (mSpannedText) {
417             if (mLineBackgroundSpans == null) {
418                 mLineBackgroundSpans = new SpanSet<LineBackgroundSpan>(LineBackgroundSpan.class);
419             }
420 
421             Spanned buffer = (Spanned) mText;
422             int textLength = buffer.length();
423             mLineBackgroundSpans.init(buffer, 0, textLength);
424 
425             if (mLineBackgroundSpans.numberOfSpans > 0) {
426                 int previousLineBottom = getLineTop(firstLine);
427                 int previousLineEnd = getLineStart(firstLine);
428                 ParagraphStyle[] spans = NO_PARA_SPANS;
429                 int spansLength = 0;
430                 TextPaint paint = mPaint;
431                 int spanEnd = 0;
432                 final int width = mWidth;
433                 for (int i = firstLine; i <= lastLine; i++) {
434                     int start = previousLineEnd;
435                     int end = getLineStart(i + 1);
436                     previousLineEnd = end;
437 
438                     int ltop = previousLineBottom;
439                     int lbottom = getLineTop(i + 1);
440                     previousLineBottom = lbottom;
441                     int lbaseline = lbottom - getLineDescent(i);
442 
443                     if (start >= spanEnd) {
444                         // These should be infrequent, so we'll use this so that
445                         // we don't have to check as often.
446                         spanEnd = mLineBackgroundSpans.getNextTransition(start, textLength);
447                         // All LineBackgroundSpans on a line contribute to its background.
448                         spansLength = 0;
449                         // Duplication of the logic of getParagraphSpans
450                         if (start != end || start == 0) {
451                             // Equivalent to a getSpans(start, end), but filling the 'spans' local
452                             // array instead to reduce memory allocation
453                             for (int j = 0; j < mLineBackgroundSpans.numberOfSpans; j++) {
454                                 // equal test is valid since both intervals are not empty by
455                                 // construction
456                                 if (mLineBackgroundSpans.spanStarts[j] >= end ||
457                                         mLineBackgroundSpans.spanEnds[j] <= start) continue;
458                                 spans = GrowingArrayUtils.append(
459                                         spans, spansLength, mLineBackgroundSpans.spans[j]);
460                                 spansLength++;
461                             }
462                         }
463                     }
464 
465                     for (int n = 0; n < spansLength; n++) {
466                         LineBackgroundSpan lineBackgroundSpan = (LineBackgroundSpan) spans[n];
467                         lineBackgroundSpan.drawBackground(canvas, paint, 0, width,
468                                 ltop, lbaseline, lbottom,
469                                 buffer, start, end, i);
470                     }
471                 }
472             }
473             mLineBackgroundSpans.recycle();
474         }
475 
476         // There can be a highlight even without spans if we are drawing
477         // a non-spanned transformation of a spanned editing buffer.
478         if (highlight != null) {
479             if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical);
480             canvas.drawPath(highlight, highlightPaint);
481             if (cursorOffsetVertical != 0) canvas.translate(0, -cursorOffsetVertical);
482         }
483     }
484 
485     /**
486      * @param canvas
487      * @return The range of lines that need to be drawn, possibly empty.
488      * @hide
489      */
getLineRangeForDraw(Canvas canvas)490     public long getLineRangeForDraw(Canvas canvas) {
491         int dtop, dbottom;
492 
493         synchronized (sTempRect) {
494             if (!canvas.getClipBounds(sTempRect)) {
495                 // Negative range end used as a special flag
496                 return TextUtils.packRangeInLong(0, -1);
497             }
498 
499             dtop = sTempRect.top;
500             dbottom = sTempRect.bottom;
501         }
502 
503         final int top = Math.max(dtop, 0);
504         final int bottom = Math.min(getLineTop(getLineCount()), dbottom);
505 
506         if (top >= bottom) return TextUtils.packRangeInLong(0, -1);
507         return TextUtils.packRangeInLong(getLineForVertical(top), getLineForVertical(bottom));
508     }
509 
510     /**
511      * Return the start position of the line, given the left and right bounds
512      * of the margins.
513      *
514      * @param line the line index
515      * @param left the left bounds (0, or leading margin if ltr para)
516      * @param right the right bounds (width, minus leading margin if rtl para)
517      * @return the start position of the line (to right of line if rtl para)
518      */
getLineStartPos(int line, int left, int right)519     private int getLineStartPos(int line, int left, int right) {
520         // Adjust the point at which to start rendering depending on the
521         // alignment of the paragraph.
522         Alignment align = getParagraphAlignment(line);
523         int dir = getParagraphDirection(line);
524 
525         if (align == Alignment.ALIGN_LEFT) {
526             align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
527         } else if (align == Alignment.ALIGN_RIGHT) {
528             align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
529         }
530 
531         int x;
532         if (align == Alignment.ALIGN_NORMAL) {
533             if (dir == DIR_LEFT_TO_RIGHT) {
534                 x = left + getIndentAdjust(line, Alignment.ALIGN_LEFT);
535             } else {
536                 x = right + getIndentAdjust(line, Alignment.ALIGN_RIGHT);
537             }
538         } else {
539             TabStops tabStops = null;
540             if (mSpannedText && getLineContainsTab(line)) {
541                 Spanned spanned = (Spanned) mText;
542                 int start = getLineStart(line);
543                 int spanEnd = spanned.nextSpanTransition(start, spanned.length(),
544                         TabStopSpan.class);
545                 TabStopSpan[] tabSpans = getParagraphSpans(spanned, start, spanEnd,
546                         TabStopSpan.class);
547                 if (tabSpans.length > 0) {
548                     tabStops = new TabStops(TAB_INCREMENT, tabSpans);
549                 }
550             }
551             int max = (int)getLineExtent(line, tabStops, false);
552             if (align == Alignment.ALIGN_OPPOSITE) {
553                 if (dir == DIR_LEFT_TO_RIGHT) {
554                     x = right - max + getIndentAdjust(line, Alignment.ALIGN_RIGHT);
555                 } else {
556                     // max is negative here
557                     x = left - max + getIndentAdjust(line, Alignment.ALIGN_LEFT);
558                 }
559             } else { // Alignment.ALIGN_CENTER
560                 max = max & ~1;
561                 x = (left + right - max) >> 1 + getIndentAdjust(line, Alignment.ALIGN_CENTER);
562             }
563         }
564         return x;
565     }
566 
567     /**
568      * Return the text that is displayed by this Layout.
569      */
getText()570     public final CharSequence getText() {
571         return mText;
572     }
573 
574     /**
575      * Return the base Paint properties for this layout.
576      * Do NOT change the paint, which may result in funny
577      * drawing for this layout.
578      */
getPaint()579     public final TextPaint getPaint() {
580         return mPaint;
581     }
582 
583     /**
584      * Return the width of this layout.
585      */
getWidth()586     public final int getWidth() {
587         return mWidth;
588     }
589 
590     /**
591      * Return the width to which this Layout is ellipsizing, or
592      * {@link #getWidth} if it is not doing anything special.
593      */
getEllipsizedWidth()594     public int getEllipsizedWidth() {
595         return mWidth;
596     }
597 
598     /**
599      * Increase the width of this layout to the specified width.
600      * Be careful to use this only when you know it is appropriate&mdash;
601      * it does not cause the text to reflow to use the full new width.
602      */
increaseWidthTo(int wid)603     public final void increaseWidthTo(int wid) {
604         if (wid < mWidth) {
605             throw new RuntimeException("attempted to reduce Layout width");
606         }
607 
608         mWidth = wid;
609     }
610 
611     /**
612      * Return the total height of this layout.
613      */
getHeight()614     public int getHeight() {
615         return getLineTop(getLineCount());
616     }
617 
618     /**
619      * Return the base alignment of this layout.
620      */
getAlignment()621     public final Alignment getAlignment() {
622         return mAlignment;
623     }
624 
625     /**
626      * Return what the text height is multiplied by to get the line height.
627      */
getSpacingMultiplier()628     public final float getSpacingMultiplier() {
629         return mSpacingMult;
630     }
631 
632     /**
633      * Return the number of units of leading that are added to each line.
634      */
getSpacingAdd()635     public final float getSpacingAdd() {
636         return mSpacingAdd;
637     }
638 
639     /**
640      * Return the heuristic used to determine paragraph text direction.
641      * @hide
642      */
getTextDirectionHeuristic()643     public final TextDirectionHeuristic getTextDirectionHeuristic() {
644         return mTextDir;
645     }
646 
647     /**
648      * Return the number of lines of text in this layout.
649      */
getLineCount()650     public abstract int getLineCount();
651 
652     /**
653      * Return the baseline for the specified line (0&hellip;getLineCount() - 1)
654      * If bounds is not null, return the top, left, right, bottom extents
655      * of the specified line in it.
656      * @param line which line to examine (0..getLineCount() - 1)
657      * @param bounds Optional. If not null, it returns the extent of the line
658      * @return the Y-coordinate of the baseline
659      */
getLineBounds(int line, Rect bounds)660     public int getLineBounds(int line, Rect bounds) {
661         if (bounds != null) {
662             bounds.left = 0;     // ???
663             bounds.top = getLineTop(line);
664             bounds.right = mWidth;   // ???
665             bounds.bottom = getLineTop(line + 1);
666         }
667         return getLineBaseline(line);
668     }
669 
670     /**
671      * Return the vertical position of the top of the specified line
672      * (0&hellip;getLineCount()).
673      * If the specified line is equal to the line count, returns the
674      * bottom of the last line.
675      */
getLineTop(int line)676     public abstract int getLineTop(int line);
677 
678     /**
679      * Return the descent of the specified line(0&hellip;getLineCount() - 1).
680      */
getLineDescent(int line)681     public abstract int getLineDescent(int line);
682 
683     /**
684      * Return the text offset of the beginning of the specified line (
685      * 0&hellip;getLineCount()). If the specified line is equal to the line
686      * count, returns the length of the text.
687      */
getLineStart(int line)688     public abstract int getLineStart(int line);
689 
690     /**
691      * Returns the primary directionality of the paragraph containing the
692      * specified line, either 1 for left-to-right lines, or -1 for right-to-left
693      * lines (see {@link #DIR_LEFT_TO_RIGHT}, {@link #DIR_RIGHT_TO_LEFT}).
694      */
getParagraphDirection(int line)695     public abstract int getParagraphDirection(int line);
696 
697     /**
698      * Returns whether the specified line contains one or more
699      * characters that need to be handled specially, like tabs.
700      */
getLineContainsTab(int line)701     public abstract boolean getLineContainsTab(int line);
702 
703     /**
704      * Returns the directional run information for the specified line.
705      * The array alternates counts of characters in left-to-right
706      * and right-to-left segments of the line.
707      *
708      * <p>NOTE: this is inadequate to support bidirectional text, and will change.
709      */
getLineDirections(int line)710     public abstract Directions getLineDirections(int line);
711 
712     /**
713      * Returns the (negative) number of extra pixels of ascent padding in the
714      * top line of the Layout.
715      */
getTopPadding()716     public abstract int getTopPadding();
717 
718     /**
719      * Returns the number of extra pixels of descent padding in the
720      * bottom line of the Layout.
721      */
getBottomPadding()722     public abstract int getBottomPadding();
723 
724     /**
725      * Returns the hyphen edit for a line.
726      *
727      * @hide
728      */
getHyphen(int line)729     public int getHyphen(int line) {
730         return 0;
731     }
732 
733     /**
734      * Returns the left indent for a line.
735      *
736      * @hide
737      */
getIndentAdjust(int line, Alignment alignment)738     public int getIndentAdjust(int line, Alignment alignment) {
739         return 0;
740     }
741 
742     /**
743      * Returns true if the character at offset and the preceding character
744      * are at different run levels (and thus there's a split caret).
745      * @param offset the offset
746      * @return true if at a level boundary
747      * @hide
748      */
isLevelBoundary(int offset)749     public boolean isLevelBoundary(int offset) {
750         int line = getLineForOffset(offset);
751         Directions dirs = getLineDirections(line);
752         if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) {
753             return false;
754         }
755 
756         int[] runs = dirs.mDirections;
757         int lineStart = getLineStart(line);
758         int lineEnd = getLineEnd(line);
759         if (offset == lineStart || offset == lineEnd) {
760             int paraLevel = getParagraphDirection(line) == 1 ? 0 : 1;
761             int runIndex = offset == lineStart ? 0 : runs.length - 2;
762             return ((runs[runIndex + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK) != paraLevel;
763         }
764 
765         offset -= lineStart;
766         for (int i = 0; i < runs.length; i += 2) {
767             if (offset == runs[i]) {
768                 return true;
769             }
770         }
771         return false;
772     }
773 
774     /**
775      * Returns true if the character at offset is right to left (RTL).
776      * @param offset the offset
777      * @return true if the character is RTL, false if it is LTR
778      */
isRtlCharAt(int offset)779     public boolean isRtlCharAt(int offset) {
780         int line = getLineForOffset(offset);
781         Directions dirs = getLineDirections(line);
782         if (dirs == DIRS_ALL_LEFT_TO_RIGHT) {
783             return false;
784         }
785         if (dirs == DIRS_ALL_RIGHT_TO_LEFT) {
786             return  true;
787         }
788         int[] runs = dirs.mDirections;
789         int lineStart = getLineStart(line);
790         for (int i = 0; i < runs.length; i += 2) {
791             int start = lineStart + runs[i];
792             int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
793             if (offset >= start && offset < limit) {
794                 int level = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
795                 return ((level & 1) != 0);
796             }
797         }
798         // Should happen only if the offset is "out of bounds"
799         return false;
800     }
801 
802     /**
803      * Returns the range of the run that the character at offset belongs to.
804      * @param offset the offset
805      * @return The range of the run
806      * @hide
807      */
getRunRange(int offset)808     public long getRunRange(int offset) {
809         int line = getLineForOffset(offset);
810         Directions dirs = getLineDirections(line);
811         if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) {
812             return TextUtils.packRangeInLong(0, getLineEnd(line));
813         }
814         int[] runs = dirs.mDirections;
815         int lineStart = getLineStart(line);
816         for (int i = 0; i < runs.length; i += 2) {
817             int start = lineStart + runs[i];
818             int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
819             if (offset >= start && offset < limit) {
820                 return TextUtils.packRangeInLong(start, limit);
821             }
822         }
823         // Should happen only if the offset is "out of bounds"
824         return TextUtils.packRangeInLong(0, getLineEnd(line));
825     }
826 
primaryIsTrailingPrevious(int offset)827     private boolean primaryIsTrailingPrevious(int offset) {
828         int line = getLineForOffset(offset);
829         int lineStart = getLineStart(line);
830         int lineEnd = getLineEnd(line);
831         int[] runs = getLineDirections(line).mDirections;
832 
833         int levelAt = -1;
834         for (int i = 0; i < runs.length; i += 2) {
835             int start = lineStart + runs[i];
836             int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
837             if (limit > lineEnd) {
838                 limit = lineEnd;
839             }
840             if (offset >= start && offset < limit) {
841                 if (offset > start) {
842                     // Previous character is at same level, so don't use trailing.
843                     return false;
844                 }
845                 levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
846                 break;
847             }
848         }
849         if (levelAt == -1) {
850             // Offset was limit of line.
851             levelAt = getParagraphDirection(line) == 1 ? 0 : 1;
852         }
853 
854         // At level boundary, check previous level.
855         int levelBefore = -1;
856         if (offset == lineStart) {
857             levelBefore = getParagraphDirection(line) == 1 ? 0 : 1;
858         } else {
859             offset -= 1;
860             for (int i = 0; i < runs.length; i += 2) {
861                 int start = lineStart + runs[i];
862                 int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
863                 if (limit > lineEnd) {
864                     limit = lineEnd;
865                 }
866                 if (offset >= start && offset < limit) {
867                     levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
868                     break;
869                 }
870             }
871         }
872 
873         return levelBefore < levelAt;
874     }
875 
876     /**
877      * Get the primary horizontal position for the specified text offset.
878      * This is the location where a new character would be inserted in
879      * the paragraph's primary direction.
880      */
getPrimaryHorizontal(int offset)881     public float getPrimaryHorizontal(int offset) {
882         return getPrimaryHorizontal(offset, false /* not clamped */);
883     }
884 
885     /**
886      * Get the primary horizontal position for the specified text offset, but
887      * optionally clamp it so that it doesn't exceed the width of the layout.
888      * @hide
889      */
getPrimaryHorizontal(int offset, boolean clamped)890     public float getPrimaryHorizontal(int offset, boolean clamped) {
891         boolean trailing = primaryIsTrailingPrevious(offset);
892         return getHorizontal(offset, trailing, clamped);
893     }
894 
895     /**
896      * Get the secondary horizontal position for the specified text offset.
897      * This is the location where a new character would be inserted in
898      * the direction other than the paragraph's primary direction.
899      */
getSecondaryHorizontal(int offset)900     public float getSecondaryHorizontal(int offset) {
901         return getSecondaryHorizontal(offset, false /* not clamped */);
902     }
903 
904     /**
905      * Get the secondary horizontal position for the specified text offset, but
906      * optionally clamp it so that it doesn't exceed the width of the layout.
907      * @hide
908      */
getSecondaryHorizontal(int offset, boolean clamped)909     public float getSecondaryHorizontal(int offset, boolean clamped) {
910         boolean trailing = primaryIsTrailingPrevious(offset);
911         return getHorizontal(offset, !trailing, clamped);
912     }
913 
getHorizontal(int offset, boolean primary)914     private float getHorizontal(int offset, boolean primary) {
915         return primary ? getPrimaryHorizontal(offset) : getSecondaryHorizontal(offset);
916     }
917 
getHorizontal(int offset, boolean trailing, boolean clamped)918     private float getHorizontal(int offset, boolean trailing, boolean clamped) {
919         int line = getLineForOffset(offset);
920 
921         return getHorizontal(offset, trailing, line, clamped);
922     }
923 
getHorizontal(int offset, boolean trailing, int line, boolean clamped)924     private float getHorizontal(int offset, boolean trailing, int line, boolean clamped) {
925         int start = getLineStart(line);
926         int end = getLineEnd(line);
927         int dir = getParagraphDirection(line);
928         boolean hasTab = getLineContainsTab(line);
929         Directions directions = getLineDirections(line);
930 
931         TabStops tabStops = null;
932         if (hasTab && mText instanceof Spanned) {
933             // Just checking this line should be good enough, tabs should be
934             // consistent across all lines in a paragraph.
935             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
936             if (tabs.length > 0) {
937                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
938             }
939         }
940 
941         TextLine tl = TextLine.obtain();
942         tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops);
943         float wid = tl.measure(offset - start, trailing, null);
944         TextLine.recycle(tl);
945 
946         if (clamped && wid > mWidth) {
947             wid = mWidth;
948         }
949         int left = getParagraphLeft(line);
950         int right = getParagraphRight(line);
951 
952         return getLineStartPos(line, left, right) + wid;
953     }
954 
955     /**
956      * Get the leftmost position that should be exposed for horizontal
957      * scrolling on the specified line.
958      */
getLineLeft(int line)959     public float getLineLeft(int line) {
960         int dir = getParagraphDirection(line);
961         Alignment align = getParagraphAlignment(line);
962 
963         if (align == Alignment.ALIGN_LEFT) {
964             return 0;
965         } else if (align == Alignment.ALIGN_NORMAL) {
966             if (dir == DIR_RIGHT_TO_LEFT)
967                 return getParagraphRight(line) - getLineMax(line);
968             else
969                 return 0;
970         } else if (align == Alignment.ALIGN_RIGHT) {
971             return mWidth - getLineMax(line);
972         } else if (align == Alignment.ALIGN_OPPOSITE) {
973             if (dir == DIR_RIGHT_TO_LEFT)
974                 return 0;
975             else
976                 return mWidth - getLineMax(line);
977         } else { /* align == Alignment.ALIGN_CENTER */
978             int left = getParagraphLeft(line);
979             int right = getParagraphRight(line);
980             int max = ((int) getLineMax(line)) & ~1;
981 
982             return left + ((right - left) - max) / 2;
983         }
984     }
985 
986     /**
987      * Get the rightmost position that should be exposed for horizontal
988      * scrolling on the specified line.
989      */
getLineRight(int line)990     public float getLineRight(int line) {
991         int dir = getParagraphDirection(line);
992         Alignment align = getParagraphAlignment(line);
993 
994         if (align == Alignment.ALIGN_LEFT) {
995             return getParagraphLeft(line) + getLineMax(line);
996         } else if (align == Alignment.ALIGN_NORMAL) {
997             if (dir == DIR_RIGHT_TO_LEFT)
998                 return mWidth;
999             else
1000                 return getParagraphLeft(line) + getLineMax(line);
1001         } else if (align == Alignment.ALIGN_RIGHT) {
1002             return mWidth;
1003         } else if (align == Alignment.ALIGN_OPPOSITE) {
1004             if (dir == DIR_RIGHT_TO_LEFT)
1005                 return getLineMax(line);
1006             else
1007                 return mWidth;
1008         } else { /* align == Alignment.ALIGN_CENTER */
1009             int left = getParagraphLeft(line);
1010             int right = getParagraphRight(line);
1011             int max = ((int) getLineMax(line)) & ~1;
1012 
1013             return right - ((right - left) - max) / 2;
1014         }
1015     }
1016 
1017     /**
1018      * Gets the unsigned horizontal extent of the specified line, including
1019      * leading margin indent, but excluding trailing whitespace.
1020      */
getLineMax(int line)1021     public float getLineMax(int line) {
1022         float margin = getParagraphLeadingMargin(line);
1023         float signedExtent = getLineExtent(line, false);
1024         return margin + (signedExtent >= 0 ? signedExtent : -signedExtent);
1025     }
1026 
1027     /**
1028      * Gets the unsigned horizontal extent of the specified line, including
1029      * leading margin indent and trailing whitespace.
1030      */
getLineWidth(int line)1031     public float getLineWidth(int line) {
1032         float margin = getParagraphLeadingMargin(line);
1033         float signedExtent = getLineExtent(line, true);
1034         return margin + (signedExtent >= 0 ? signedExtent : -signedExtent);
1035     }
1036 
1037     /**
1038      * Like {@link #getLineExtent(int,TabStops,boolean)} but determines the
1039      * tab stops instead of using the ones passed in.
1040      * @param line the index of the line
1041      * @param full whether to include trailing whitespace
1042      * @return the extent of the line
1043      */
getLineExtent(int line, boolean full)1044     private float getLineExtent(int line, boolean full) {
1045         int start = getLineStart(line);
1046         int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
1047 
1048         boolean hasTabs = getLineContainsTab(line);
1049         TabStops tabStops = null;
1050         if (hasTabs && mText instanceof Spanned) {
1051             // Just checking this line should be good enough, tabs should be
1052             // consistent across all lines in a paragraph.
1053             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
1054             if (tabs.length > 0) {
1055                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
1056             }
1057         }
1058         Directions directions = getLineDirections(line);
1059         // Returned directions can actually be null
1060         if (directions == null) {
1061             return 0f;
1062         }
1063         int dir = getParagraphDirection(line);
1064 
1065         TextLine tl = TextLine.obtain();
1066         tl.set(mPaint, mText, start, end, dir, directions, hasTabs, tabStops);
1067         float width = tl.metrics(null);
1068         TextLine.recycle(tl);
1069         return width;
1070     }
1071 
1072     /**
1073      * Returns the signed horizontal extent of the specified line, excluding
1074      * leading margin.  If full is false, excludes trailing whitespace.
1075      * @param line the index of the line
1076      * @param tabStops the tab stops, can be null if we know they're not used.
1077      * @param full whether to include trailing whitespace
1078      * @return the extent of the text on this line
1079      */
getLineExtent(int line, TabStops tabStops, boolean full)1080     private float getLineExtent(int line, TabStops tabStops, boolean full) {
1081         int start = getLineStart(line);
1082         int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
1083         boolean hasTabs = getLineContainsTab(line);
1084         Directions directions = getLineDirections(line);
1085         int dir = getParagraphDirection(line);
1086 
1087         TextLine tl = TextLine.obtain();
1088         tl.set(mPaint, mText, start, end, dir, directions, hasTabs, tabStops);
1089         float width = tl.metrics(null);
1090         TextLine.recycle(tl);
1091         return width;
1092     }
1093 
1094     /**
1095      * Get the line number corresponding to the specified vertical position.
1096      * If you ask for a position above 0, you get 0; if you ask for a position
1097      * below the bottom of the text, you get the last line.
1098      */
1099     // FIXME: It may be faster to do a linear search for layouts without many lines.
getLineForVertical(int vertical)1100     public int getLineForVertical(int vertical) {
1101         int high = getLineCount(), low = -1, guess;
1102 
1103         while (high - low > 1) {
1104             guess = (high + low) / 2;
1105 
1106             if (getLineTop(guess) > vertical)
1107                 high = guess;
1108             else
1109                 low = guess;
1110         }
1111 
1112         if (low < 0)
1113             return 0;
1114         else
1115             return low;
1116     }
1117 
1118     /**
1119      * Get the line number on which the specified text offset appears.
1120      * If you ask for a position before 0, you get 0; if you ask for a position
1121      * beyond the end of the text, you get the last line.
1122      */
getLineForOffset(int offset)1123     public int getLineForOffset(int offset) {
1124         int high = getLineCount(), low = -1, guess;
1125 
1126         while (high - low > 1) {
1127             guess = (high + low) / 2;
1128 
1129             if (getLineStart(guess) > offset)
1130                 high = guess;
1131             else
1132                 low = guess;
1133         }
1134 
1135         if (low < 0)
1136             return 0;
1137         else
1138             return low;
1139     }
1140 
1141     /**
1142      * Get the character offset on the specified line whose position is
1143      * closest to the specified horizontal position.
1144      */
getOffsetForHorizontal(int line, float horiz)1145     public int getOffsetForHorizontal(int line, float horiz) {
1146         return getOffsetForHorizontal(line, horiz, true);
1147     }
1148 
1149     /**
1150      * Get the character offset on the specified line whose position is
1151      * closest to the specified horizontal position.
1152      *
1153      * @param line the line used to find the closest offset
1154      * @param horiz the horizontal position used to find the closest offset
1155      * @param primary whether to use the primary position or secondary position to find the offset
1156      *
1157      * @hide
1158      */
getOffsetForHorizontal(int line, float horiz, boolean primary)1159     public int getOffsetForHorizontal(int line, float horiz, boolean primary) {
1160         // TODO: use Paint.getOffsetForAdvance to avoid binary search
1161         final int lineEndOffset = getLineEnd(line);
1162         final int lineStartOffset = getLineStart(line);
1163 
1164         Directions dirs = getLineDirections(line);
1165 
1166         TextLine tl = TextLine.obtain();
1167         // XXX: we don't care about tabs as we just use TextLine#getOffsetToLeftRightOf here.
1168         tl.set(mPaint, mText, lineStartOffset, lineEndOffset, getParagraphDirection(line), dirs,
1169                 false, null);
1170 
1171         final int max;
1172         if (line == getLineCount() - 1) {
1173             max = lineEndOffset;
1174         } else {
1175             max = tl.getOffsetToLeftRightOf(lineEndOffset - lineStartOffset,
1176                     !isRtlCharAt(lineEndOffset - 1)) + lineStartOffset;
1177         }
1178         int best = lineStartOffset;
1179         float bestdist = Math.abs(getHorizontal(best, primary) - horiz);
1180 
1181         for (int i = 0; i < dirs.mDirections.length; i += 2) {
1182             int here = lineStartOffset + dirs.mDirections[i];
1183             int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK);
1184             boolean isRtl = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0;
1185             int swap = isRtl ? -1 : 1;
1186 
1187             if (there > max)
1188                 there = max;
1189             int high = there - 1 + 1, low = here + 1 - 1, guess;
1190 
1191             while (high - low > 1) {
1192                 guess = (high + low) / 2;
1193                 int adguess = getOffsetAtStartOf(guess);
1194 
1195                 if (getHorizontal(adguess, primary) * swap >= horiz * swap)
1196                     high = guess;
1197                 else
1198                     low = guess;
1199             }
1200 
1201             if (low < here + 1)
1202                 low = here + 1;
1203 
1204             if (low < there) {
1205                 int aft = tl.getOffsetToLeftRightOf(low - lineStartOffset, isRtl) + lineStartOffset;
1206                 low = tl.getOffsetToLeftRightOf(aft - lineStartOffset, !isRtl) + lineStartOffset;
1207                 if (low >= here && low < there) {
1208                     float dist = Math.abs(getHorizontal(low, primary) - horiz);
1209                     if (aft < there) {
1210                         float other = Math.abs(getHorizontal(aft, primary) - horiz);
1211 
1212                         if (other < dist) {
1213                             dist = other;
1214                             low = aft;
1215                         }
1216                     }
1217 
1218                     if (dist < bestdist) {
1219                         bestdist = dist;
1220                         best = low;
1221                     }
1222                 }
1223             }
1224 
1225             float dist = Math.abs(getHorizontal(here, primary) - horiz);
1226 
1227             if (dist < bestdist) {
1228                 bestdist = dist;
1229                 best = here;
1230             }
1231         }
1232 
1233         float dist = Math.abs(getHorizontal(max, primary) - horiz);
1234 
1235         if (dist <= bestdist) {
1236             bestdist = dist;
1237             best = max;
1238         }
1239 
1240         TextLine.recycle(tl);
1241         return best;
1242     }
1243 
1244     /**
1245      * Return the text offset after the last character on the specified line.
1246      */
getLineEnd(int line)1247     public final int getLineEnd(int line) {
1248         return getLineStart(line + 1);
1249     }
1250 
1251     /**
1252      * Return the text offset after the last visible character (so whitespace
1253      * is not counted) on the specified line.
1254      */
getLineVisibleEnd(int line)1255     public int getLineVisibleEnd(int line) {
1256         return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1));
1257     }
1258 
getLineVisibleEnd(int line, int start, int end)1259     private int getLineVisibleEnd(int line, int start, int end) {
1260         CharSequence text = mText;
1261         char ch;
1262         if (line == getLineCount() - 1) {
1263             return end;
1264         }
1265 
1266         for (; end > start; end--) {
1267             ch = text.charAt(end - 1);
1268 
1269             if (ch == '\n') {
1270                 return end - 1;
1271             }
1272 
1273             // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
1274             if (!(ch == ' ' || ch == '\t' || ch == 0x1680 ||
1275                     (0x2000 <= ch && ch <= 0x200A && ch != 0x2007) ||
1276                     ch == 0x205F || ch == 0x3000)) {
1277                 break;
1278             }
1279 
1280         }
1281 
1282         return end;
1283     }
1284 
1285     /**
1286      * Return the vertical position of the bottom of the specified line.
1287      */
getLineBottom(int line)1288     public final int getLineBottom(int line) {
1289         return getLineTop(line + 1);
1290     }
1291 
1292     /**
1293      * Return the vertical position of the baseline of the specified line.
1294      */
getLineBaseline(int line)1295     public final int getLineBaseline(int line) {
1296         // getLineTop(line+1) == getLineTop(line)
1297         return getLineTop(line+1) - getLineDescent(line);
1298     }
1299 
1300     /**
1301      * Get the ascent of the text on the specified line.
1302      * The return value is negative to match the Paint.ascent() convention.
1303      */
getLineAscent(int line)1304     public final int getLineAscent(int line) {
1305         // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line)
1306         return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line));
1307     }
1308 
getOffsetToLeftOf(int offset)1309     public int getOffsetToLeftOf(int offset) {
1310         return getOffsetToLeftRightOf(offset, true);
1311     }
1312 
getOffsetToRightOf(int offset)1313     public int getOffsetToRightOf(int offset) {
1314         return getOffsetToLeftRightOf(offset, false);
1315     }
1316 
getOffsetToLeftRightOf(int caret, boolean toLeft)1317     private int getOffsetToLeftRightOf(int caret, boolean toLeft) {
1318         int line = getLineForOffset(caret);
1319         int lineStart = getLineStart(line);
1320         int lineEnd = getLineEnd(line);
1321         int lineDir = getParagraphDirection(line);
1322 
1323         boolean lineChanged = false;
1324         boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT);
1325         // if walking off line, look at the line we're headed to
1326         if (advance) {
1327             if (caret == lineEnd) {
1328                 if (line < getLineCount() - 1) {
1329                     lineChanged = true;
1330                     ++line;
1331                 } else {
1332                     return caret; // at very end, don't move
1333                 }
1334             }
1335         } else {
1336             if (caret == lineStart) {
1337                 if (line > 0) {
1338                     lineChanged = true;
1339                     --line;
1340                 } else {
1341                     return caret; // at very start, don't move
1342                 }
1343             }
1344         }
1345 
1346         if (lineChanged) {
1347             lineStart = getLineStart(line);
1348             lineEnd = getLineEnd(line);
1349             int newDir = getParagraphDirection(line);
1350             if (newDir != lineDir) {
1351                 // unusual case.  we want to walk onto the line, but it runs
1352                 // in a different direction than this one, so we fake movement
1353                 // in the opposite direction.
1354                 toLeft = !toLeft;
1355                 lineDir = newDir;
1356             }
1357         }
1358 
1359         Directions directions = getLineDirections(line);
1360 
1361         TextLine tl = TextLine.obtain();
1362         // XXX: we don't care about tabs
1363         tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null);
1364         caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft);
1365         tl = TextLine.recycle(tl);
1366         return caret;
1367     }
1368 
getOffsetAtStartOf(int offset)1369     private int getOffsetAtStartOf(int offset) {
1370         // XXX this probably should skip local reorderings and
1371         // zero-width characters, look at callers
1372         if (offset == 0)
1373             return 0;
1374 
1375         CharSequence text = mText;
1376         char c = text.charAt(offset);
1377 
1378         if (c >= '\uDC00' && c <= '\uDFFF') {
1379             char c1 = text.charAt(offset - 1);
1380 
1381             if (c1 >= '\uD800' && c1 <= '\uDBFF')
1382                 offset -= 1;
1383         }
1384 
1385         if (mSpannedText) {
1386             ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
1387                                                        ReplacementSpan.class);
1388 
1389             for (int i = 0; i < spans.length; i++) {
1390                 int start = ((Spanned) text).getSpanStart(spans[i]);
1391                 int end = ((Spanned) text).getSpanEnd(spans[i]);
1392 
1393                 if (start < offset && end > offset)
1394                     offset = start;
1395             }
1396         }
1397 
1398         return offset;
1399     }
1400 
1401     /**
1402      * Determine whether we should clamp cursor position. Currently it's
1403      * only robust for left-aligned displays.
1404      * @hide
1405      */
shouldClampCursor(int line)1406     public boolean shouldClampCursor(int line) {
1407         // Only clamp cursor position in left-aligned displays.
1408         switch (getParagraphAlignment(line)) {
1409             case ALIGN_LEFT:
1410                 return true;
1411             case ALIGN_NORMAL:
1412                 return getParagraphDirection(line) > 0;
1413             default:
1414                 return false;
1415         }
1416 
1417     }
1418     /**
1419      * Fills in the specified Path with a representation of a cursor
1420      * at the specified offset.  This will often be a vertical line
1421      * but can be multiple discontinuous lines in text with multiple
1422      * directionalities.
1423      */
getCursorPath(int point, Path dest, CharSequence editingBuffer)1424     public void getCursorPath(int point, Path dest,
1425                               CharSequence editingBuffer) {
1426         dest.reset();
1427 
1428         int line = getLineForOffset(point);
1429         int top = getLineTop(line);
1430         int bottom = getLineTop(line+1);
1431 
1432         boolean clamped = shouldClampCursor(line);
1433         float h1 = getPrimaryHorizontal(point, clamped) - 0.5f;
1434         float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point, clamped) - 0.5f : h1;
1435 
1436         int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) |
1437                    TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING);
1438         int fn = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_ALT_ON);
1439         int dist = 0;
1440 
1441         if (caps != 0 || fn != 0) {
1442             dist = (bottom - top) >> 2;
1443 
1444             if (fn != 0)
1445                 top += dist;
1446             if (caps != 0)
1447                 bottom -= dist;
1448         }
1449 
1450         if (h1 < 0.5f)
1451             h1 = 0.5f;
1452         if (h2 < 0.5f)
1453             h2 = 0.5f;
1454 
1455         if (Float.compare(h1, h2) == 0) {
1456             dest.moveTo(h1, top);
1457             dest.lineTo(h1, bottom);
1458         } else {
1459             dest.moveTo(h1, top);
1460             dest.lineTo(h1, (top + bottom) >> 1);
1461 
1462             dest.moveTo(h2, (top + bottom) >> 1);
1463             dest.lineTo(h2, bottom);
1464         }
1465 
1466         if (caps == 2) {
1467             dest.moveTo(h2, bottom);
1468             dest.lineTo(h2 - dist, bottom + dist);
1469             dest.lineTo(h2, bottom);
1470             dest.lineTo(h2 + dist, bottom + dist);
1471         } else if (caps == 1) {
1472             dest.moveTo(h2, bottom);
1473             dest.lineTo(h2 - dist, bottom + dist);
1474 
1475             dest.moveTo(h2 - dist, bottom + dist - 0.5f);
1476             dest.lineTo(h2 + dist, bottom + dist - 0.5f);
1477 
1478             dest.moveTo(h2 + dist, bottom + dist);
1479             dest.lineTo(h2, bottom);
1480         }
1481 
1482         if (fn == 2) {
1483             dest.moveTo(h1, top);
1484             dest.lineTo(h1 - dist, top - dist);
1485             dest.lineTo(h1, top);
1486             dest.lineTo(h1 + dist, top - dist);
1487         } else if (fn == 1) {
1488             dest.moveTo(h1, top);
1489             dest.lineTo(h1 - dist, top - dist);
1490 
1491             dest.moveTo(h1 - dist, top - dist + 0.5f);
1492             dest.lineTo(h1 + dist, top - dist + 0.5f);
1493 
1494             dest.moveTo(h1 + dist, top - dist);
1495             dest.lineTo(h1, top);
1496         }
1497     }
1498 
addSelection(int line, int start, int end, int top, int bottom, Path dest)1499     private void addSelection(int line, int start, int end,
1500                               int top, int bottom, Path dest) {
1501         int linestart = getLineStart(line);
1502         int lineend = getLineEnd(line);
1503         Directions dirs = getLineDirections(line);
1504 
1505         if (lineend > linestart && mText.charAt(lineend - 1) == '\n')
1506             lineend--;
1507 
1508         for (int i = 0; i < dirs.mDirections.length; i += 2) {
1509             int here = linestart + dirs.mDirections[i];
1510             int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK);
1511 
1512             if (there > lineend)
1513                 there = lineend;
1514 
1515             if (start <= there && end >= here) {
1516                 int st = Math.max(start, here);
1517                 int en = Math.min(end, there);
1518 
1519                 if (st != en) {
1520                     float h1 = getHorizontal(st, false, line, false /* not clamped */);
1521                     float h2 = getHorizontal(en, true, line, false /* not clamped */);
1522 
1523                     float left = Math.min(h1, h2);
1524                     float right = Math.max(h1, h2);
1525 
1526                     dest.addRect(left, top, right, bottom, Path.Direction.CW);
1527                 }
1528             }
1529         }
1530     }
1531 
1532     /**
1533      * Fills in the specified Path with a representation of a highlight
1534      * between the specified offsets.  This will often be a rectangle
1535      * or a potentially discontinuous set of rectangles.  If the start
1536      * and end are the same, the returned path is empty.
1537      */
getSelectionPath(int start, int end, Path dest)1538     public void getSelectionPath(int start, int end, Path dest) {
1539         dest.reset();
1540 
1541         if (start == end)
1542             return;
1543 
1544         if (end < start) {
1545             int temp = end;
1546             end = start;
1547             start = temp;
1548         }
1549 
1550         int startline = getLineForOffset(start);
1551         int endline = getLineForOffset(end);
1552 
1553         int top = getLineTop(startline);
1554         int bottom = getLineBottom(endline);
1555 
1556         if (startline == endline) {
1557             addSelection(startline, start, end, top, bottom, dest);
1558         } else {
1559             final float width = mWidth;
1560 
1561             addSelection(startline, start, getLineEnd(startline),
1562                          top, getLineBottom(startline), dest);
1563 
1564             if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT)
1565                 dest.addRect(getLineLeft(startline), top,
1566                               0, getLineBottom(startline), Path.Direction.CW);
1567             else
1568                 dest.addRect(getLineRight(startline), top,
1569                               width, getLineBottom(startline), Path.Direction.CW);
1570 
1571             for (int i = startline + 1; i < endline; i++) {
1572                 top = getLineTop(i);
1573                 bottom = getLineBottom(i);
1574                 dest.addRect(0, top, width, bottom, Path.Direction.CW);
1575             }
1576 
1577             top = getLineTop(endline);
1578             bottom = getLineBottom(endline);
1579 
1580             addSelection(endline, getLineStart(endline), end,
1581                          top, bottom, dest);
1582 
1583             if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT)
1584                 dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW);
1585             else
1586                 dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW);
1587         }
1588     }
1589 
1590     /**
1591      * Get the alignment of the specified paragraph, taking into account
1592      * markup attached to it.
1593      */
getParagraphAlignment(int line)1594     public final Alignment getParagraphAlignment(int line) {
1595         Alignment align = mAlignment;
1596 
1597         if (mSpannedText) {
1598             Spanned sp = (Spanned) mText;
1599             AlignmentSpan[] spans = getParagraphSpans(sp, getLineStart(line),
1600                                                 getLineEnd(line),
1601                                                 AlignmentSpan.class);
1602 
1603             int spanLength = spans.length;
1604             if (spanLength > 0) {
1605                 align = spans[spanLength-1].getAlignment();
1606             }
1607         }
1608 
1609         return align;
1610     }
1611 
1612     /**
1613      * Get the left edge of the specified paragraph, inset by left margins.
1614      */
getParagraphLeft(int line)1615     public final int getParagraphLeft(int line) {
1616         int left = 0;
1617         int dir = getParagraphDirection(line);
1618         if (dir == DIR_RIGHT_TO_LEFT || !mSpannedText) {
1619             return left; // leading margin has no impact, or no styles
1620         }
1621         return getParagraphLeadingMargin(line);
1622     }
1623 
1624     /**
1625      * Get the right edge of the specified paragraph, inset by right margins.
1626      */
getParagraphRight(int line)1627     public final int getParagraphRight(int line) {
1628         int right = mWidth;
1629         int dir = getParagraphDirection(line);
1630         if (dir == DIR_LEFT_TO_RIGHT || !mSpannedText) {
1631             return right; // leading margin has no impact, or no styles
1632         }
1633         return right - getParagraphLeadingMargin(line);
1634     }
1635 
1636     /**
1637      * Returns the effective leading margin (unsigned) for this line,
1638      * taking into account LeadingMarginSpan and LeadingMarginSpan2.
1639      * @param line the line index
1640      * @return the leading margin of this line
1641      */
getParagraphLeadingMargin(int line)1642     private int getParagraphLeadingMargin(int line) {
1643         if (!mSpannedText) {
1644             return 0;
1645         }
1646         Spanned spanned = (Spanned) mText;
1647 
1648         int lineStart = getLineStart(line);
1649         int lineEnd = getLineEnd(line);
1650         int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd,
1651                 LeadingMarginSpan.class);
1652         LeadingMarginSpan[] spans = getParagraphSpans(spanned, lineStart, spanEnd,
1653                                                 LeadingMarginSpan.class);
1654         if (spans.length == 0) {
1655             return 0; // no leading margin span;
1656         }
1657 
1658         int margin = 0;
1659 
1660         boolean isFirstParaLine = lineStart == 0 ||
1661             spanned.charAt(lineStart - 1) == '\n';
1662 
1663         boolean useFirstLineMargin = isFirstParaLine;
1664         for (int i = 0; i < spans.length; i++) {
1665             if (spans[i] instanceof LeadingMarginSpan2) {
1666                 int spStart = spanned.getSpanStart(spans[i]);
1667                 int spanLine = getLineForOffset(spStart);
1668                 int count = ((LeadingMarginSpan2) spans[i]).getLeadingMarginLineCount();
1669                 // if there is more than one LeadingMarginSpan2, use the count that is greatest
1670                 useFirstLineMargin |= line < spanLine + count;
1671             }
1672         }
1673         for (int i = 0; i < spans.length; i++) {
1674             LeadingMarginSpan span = spans[i];
1675             margin += span.getLeadingMargin(useFirstLineMargin);
1676         }
1677 
1678         return margin;
1679     }
1680 
1681     /* package */
1682     static float measurePara(TextPaint paint, CharSequence text, int start, int end) {
1683 
1684         MeasuredText mt = MeasuredText.obtain();
1685         TextLine tl = TextLine.obtain();
1686         try {
1687             mt.setPara(text, start, end, TextDirectionHeuristics.LTR, null);
1688             Directions directions;
1689             int dir;
1690             if (mt.mEasy) {
1691                 directions = DIRS_ALL_LEFT_TO_RIGHT;
1692                 dir = Layout.DIR_LEFT_TO_RIGHT;
1693             } else {
1694                 directions = AndroidBidi.directions(mt.mDir, mt.mLevels,
1695                     0, mt.mChars, 0, mt.mLen);
1696                 dir = mt.mDir;
1697             }
1698             char[] chars = mt.mChars;
1699             int len = mt.mLen;
1700             boolean hasTabs = false;
1701             TabStops tabStops = null;
1702             // leading margins should be taken into account when measuring a paragraph
1703             int margin = 0;
1704             if (text instanceof Spanned) {
1705                 Spanned spanned = (Spanned) text;
1706                 LeadingMarginSpan[] spans = getParagraphSpans(spanned, start, end,
1707                         LeadingMarginSpan.class);
1708                 for (LeadingMarginSpan lms : spans) {
1709                     margin += lms.getLeadingMargin(true);
1710                 }
1711             }
1712             for (int i = 0; i < len; ++i) {
1713                 if (chars[i] == '\t') {
1714                     hasTabs = true;
1715                     if (text instanceof Spanned) {
1716                         Spanned spanned = (Spanned) text;
1717                         int spanEnd = spanned.nextSpanTransition(start, end,
1718                                 TabStopSpan.class);
1719                         TabStopSpan[] spans = getParagraphSpans(spanned, start, spanEnd,
1720                                 TabStopSpan.class);
1721                         if (spans.length > 0) {
1722                             tabStops = new TabStops(TAB_INCREMENT, spans);
1723                         }
1724                     }
1725                     break;
1726                 }
1727             }
1728             tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops);
1729             return margin + tl.metrics(null);
1730         } finally {
1731             TextLine.recycle(tl);
1732             MeasuredText.recycle(mt);
1733         }
1734     }
1735 
1736     /**
1737      * @hide
1738      */
1739     /* package */ static class TabStops {
1740         private int[] mStops;
1741         private int mNumStops;
1742         private int mIncrement;
1743 
1744         TabStops(int increment, Object[] spans) {
1745             reset(increment, spans);
1746         }
1747 
1748         void reset(int increment, Object[] spans) {
1749             this.mIncrement = increment;
1750 
1751             int ns = 0;
1752             if (spans != null) {
1753                 int[] stops = this.mStops;
1754                 for (Object o : spans) {
1755                     if (o instanceof TabStopSpan) {
1756                         if (stops == null) {
1757                             stops = new int[10];
1758                         } else if (ns == stops.length) {
1759                             int[] nstops = new int[ns * 2];
1760                             for (int i = 0; i < ns; ++i) {
1761                                 nstops[i] = stops[i];
1762                             }
1763                             stops = nstops;
1764                         }
1765                         stops[ns++] = ((TabStopSpan) o).getTabStop();
1766                     }
1767                 }
1768                 if (ns > 1) {
1769                     Arrays.sort(stops, 0, ns);
1770                 }
1771                 if (stops != this.mStops) {
1772                     this.mStops = stops;
1773                 }
1774             }
1775             this.mNumStops = ns;
1776         }
1777 
1778         float nextTab(float h) {
1779             int ns = this.mNumStops;
1780             if (ns > 0) {
1781                 int[] stops = this.mStops;
1782                 for (int i = 0; i < ns; ++i) {
1783                     int stop = stops[i];
1784                     if (stop > h) {
1785                         return stop;
1786                     }
1787                 }
1788             }
1789             return nextDefaultStop(h, mIncrement);
1790         }
1791 
1792         public static float nextDefaultStop(float h, int inc) {
1793             return ((int) ((h + inc) / inc)) * inc;
1794         }
1795     }
1796 
1797     /**
1798      * Returns the position of the next tab stop after h on the line.
1799      *
1800      * @param text the text
1801      * @param start start of the line
1802      * @param end limit of the line
1803      * @param h the current horizontal offset
1804      * @param tabs the tabs, can be null.  If it is null, any tabs in effect
1805      * on the line will be used.  If there are no tabs, a default offset
1806      * will be used to compute the tab stop.
1807      * @return the offset of the next tab stop.
1808      */
1809     /* package */ static float nextTab(CharSequence text, int start, int end,
1810                                        float h, Object[] tabs) {
1811         float nh = Float.MAX_VALUE;
1812         boolean alltabs = false;
1813 
1814         if (text instanceof Spanned) {
1815             if (tabs == null) {
1816                 tabs = getParagraphSpans((Spanned) text, start, end, TabStopSpan.class);
1817                 alltabs = true;
1818             }
1819 
1820             for (int i = 0; i < tabs.length; i++) {
1821                 if (!alltabs) {
1822                     if (!(tabs[i] instanceof TabStopSpan))
1823                         continue;
1824                 }
1825 
1826                 int where = ((TabStopSpan) tabs[i]).getTabStop();
1827 
1828                 if (where < nh && where > h)
1829                     nh = where;
1830             }
1831 
1832             if (nh != Float.MAX_VALUE)
1833                 return nh;
1834         }
1835 
1836         return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT;
1837     }
1838 
1839     protected final boolean isSpanned() {
1840         return mSpannedText;
1841     }
1842 
1843     /**
1844      * Returns the same as <code>text.getSpans()</code>, except where
1845      * <code>start</code> and <code>end</code> are the same and are not
1846      * at the very beginning of the text, in which case an empty array
1847      * is returned instead.
1848      * <p>
1849      * This is needed because of the special case that <code>getSpans()</code>
1850      * on an empty range returns the spans adjacent to that range, which is
1851      * primarily for the sake of <code>TextWatchers</code> so they will get
1852      * notifications when text goes from empty to non-empty.  But it also
1853      * has the unfortunate side effect that if the text ends with an empty
1854      * paragraph, that paragraph accidentally picks up the styles of the
1855      * preceding paragraph (even though those styles will not be picked up
1856      * by new text that is inserted into the empty paragraph).
1857      * <p>
1858      * The reason it just checks whether <code>start</code> and <code>end</code>
1859      * is the same is that the only time a line can contain 0 characters
1860      * is if it is the final paragraph of the Layout; otherwise any line will
1861      * contain at least one printing or newline character.  The reason for the
1862      * additional check if <code>start</code> is greater than 0 is that
1863      * if the empty paragraph is the entire content of the buffer, paragraph
1864      * styles that are already applied to the buffer will apply to text that
1865      * is inserted into it.
1866      */
1867     /* package */static <T> T[] getParagraphSpans(Spanned text, int start, int end, Class<T> type) {
1868         if (start == end && start > 0) {
1869             return ArrayUtils.emptyArray(type);
1870         }
1871 
1872         if(text instanceof SpannableStringBuilder) {
1873             return ((SpannableStringBuilder) text).getSpans(start, end, type, false);
1874         } else {
1875             return text.getSpans(start, end, type);
1876         }
1877     }
1878 
1879     private char getEllipsisChar(TextUtils.TruncateAt method) {
1880         return (method == TextUtils.TruncateAt.END_SMALL) ?
1881                 TextUtils.ELLIPSIS_TWO_DOTS[0] :
1882                 TextUtils.ELLIPSIS_NORMAL[0];
1883     }
1884 
1885     private void ellipsize(int start, int end, int line,
1886                            char[] dest, int destoff, TextUtils.TruncateAt method) {
1887         int ellipsisCount = getEllipsisCount(line);
1888 
1889         if (ellipsisCount == 0) {
1890             return;
1891         }
1892 
1893         int ellipsisStart = getEllipsisStart(line);
1894         int linestart = getLineStart(line);
1895 
1896         for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) {
1897             char c;
1898 
1899             if (i == ellipsisStart) {
1900                 c = getEllipsisChar(method); // ellipsis
1901             } else {
1902                 c = '\uFEFF'; // 0-width space
1903             }
1904 
1905             int a = i + linestart;
1906 
1907             if (a >= start && a < end) {
1908                 dest[destoff + a - start] = c;
1909             }
1910         }
1911     }
1912 
1913     /**
1914      * Stores information about bidirectional (left-to-right or right-to-left)
1915      * text within the layout of a line.
1916      */
1917     public static class Directions {
1918         // Directions represents directional runs within a line of text.
1919         // Runs are pairs of ints listed in visual order, starting from the
1920         // leading margin.  The first int of each pair is the offset from
1921         // the first character of the line to the start of the run.  The
1922         // second int represents both the length and level of the run.
1923         // The length is in the lower bits, accessed by masking with
1924         // DIR_LENGTH_MASK.  The level is in the higher bits, accessed
1925         // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK.
1926         // To simply test for an RTL direction, test the bit using
1927         // DIR_RTL_FLAG, if set then the direction is rtl.
1928 
1929         /* package */ int[] mDirections;
1930         /* package */ Directions(int[] dirs) {
1931             mDirections = dirs;
1932         }
1933     }
1934 
1935     /**
1936      * Return the offset of the first character to be ellipsized away,
1937      * relative to the start of the line.  (So 0 if the beginning of the
1938      * line is ellipsized, not getLineStart().)
1939      */
1940     public abstract int getEllipsisStart(int line);
1941 
1942     /**
1943      * Returns the number of characters to be ellipsized away, or 0 if
1944      * no ellipsis is to take place.
1945      */
1946     public abstract int getEllipsisCount(int line);
1947 
1948     /* package */ static class Ellipsizer implements CharSequence, GetChars {
1949         /* package */ CharSequence mText;
1950         /* package */ Layout mLayout;
1951         /* package */ int mWidth;
1952         /* package */ TextUtils.TruncateAt mMethod;
1953 
1954         public Ellipsizer(CharSequence s) {
1955             mText = s;
1956         }
1957 
1958         public char charAt(int off) {
1959             char[] buf = TextUtils.obtain(1);
1960             getChars(off, off + 1, buf, 0);
1961             char ret = buf[0];
1962 
1963             TextUtils.recycle(buf);
1964             return ret;
1965         }
1966 
1967         public void getChars(int start, int end, char[] dest, int destoff) {
1968             int line1 = mLayout.getLineForOffset(start);
1969             int line2 = mLayout.getLineForOffset(end);
1970 
1971             TextUtils.getChars(mText, start, end, dest, destoff);
1972 
1973             for (int i = line1; i <= line2; i++) {
1974                 mLayout.ellipsize(start, end, i, dest, destoff, mMethod);
1975             }
1976         }
1977 
1978         public int length() {
1979             return mText.length();
1980         }
1981 
1982         public CharSequence subSequence(int start, int end) {
1983             char[] s = new char[end - start];
1984             getChars(start, end, s, 0);
1985             return new String(s);
1986         }
1987 
1988         @Override
1989         public String toString() {
1990             char[] s = new char[length()];
1991             getChars(0, length(), s, 0);
1992             return new String(s);
1993         }
1994 
1995     }
1996 
1997     /* package */ static class SpannedEllipsizer extends Ellipsizer implements Spanned {
1998         private Spanned mSpanned;
1999 
2000         public SpannedEllipsizer(CharSequence display) {
2001             super(display);
2002             mSpanned = (Spanned) display;
2003         }
2004 
2005         public <T> T[] getSpans(int start, int end, Class<T> type) {
2006             return mSpanned.getSpans(start, end, type);
2007         }
2008 
2009         public int getSpanStart(Object tag) {
2010             return mSpanned.getSpanStart(tag);
2011         }
2012 
2013         public int getSpanEnd(Object tag) {
2014             return mSpanned.getSpanEnd(tag);
2015         }
2016 
2017         public int getSpanFlags(Object tag) {
2018             return mSpanned.getSpanFlags(tag);
2019         }
2020 
2021         @SuppressWarnings("rawtypes")
2022         public int nextSpanTransition(int start, int limit, Class type) {
2023             return mSpanned.nextSpanTransition(start, limit, type);
2024         }
2025 
2026         @Override
2027         public CharSequence subSequence(int start, int end) {
2028             char[] s = new char[end - start];
2029             getChars(start, end, s, 0);
2030 
2031             SpannableString ss = new SpannableString(new String(s));
2032             TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0);
2033             return ss;
2034         }
2035     }
2036 
2037     private CharSequence mText;
2038     private TextPaint mPaint;
2039     private int mWidth;
2040     private Alignment mAlignment = Alignment.ALIGN_NORMAL;
2041     private float mSpacingMult;
2042     private float mSpacingAdd;
2043     private static final Rect sTempRect = new Rect();
2044     private boolean mSpannedText;
2045     private TextDirectionHeuristic mTextDir;
2046     private SpanSet<LineBackgroundSpan> mLineBackgroundSpans;
2047 
2048     public static final int DIR_LEFT_TO_RIGHT = 1;
2049     public static final int DIR_RIGHT_TO_LEFT = -1;
2050 
2051     /* package */ static final int DIR_REQUEST_LTR = 1;
2052     /* package */ static final int DIR_REQUEST_RTL = -1;
2053     /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2;
2054     /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2;
2055 
2056     /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff;
2057     /* package */ static final int RUN_LEVEL_SHIFT = 26;
2058     /* package */ static final int RUN_LEVEL_MASK = 0x3f;
2059     /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT;
2060 
2061     public enum Alignment {
2062         ALIGN_NORMAL,
2063         ALIGN_OPPOSITE,
2064         ALIGN_CENTER,
2065         /** @hide */
2066         ALIGN_LEFT,
2067         /** @hide */
2068         ALIGN_RIGHT,
2069     }
2070 
2071     private static final int TAB_INCREMENT = 20;
2072 
2073     /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT =
2074         new Directions(new int[] { 0, RUN_LENGTH_MASK });
2075     /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT =
2076         new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG });
2077 
2078 }
2079