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