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