• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.compat.annotation.UnsupportedAppUsage;
24 import android.graphics.Canvas;
25 import android.graphics.Paint;
26 import android.graphics.Path;
27 import android.graphics.Rect;
28 import android.graphics.RectF;
29 import android.graphics.text.LineBreaker;
30 import android.os.Build;
31 import android.text.method.TextKeyListener;
32 import android.text.style.AlignmentSpan;
33 import android.text.style.LeadingMarginSpan;
34 import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
35 import android.text.style.LineBackgroundSpan;
36 import android.text.style.ParagraphStyle;
37 import android.text.style.ReplacementSpan;
38 import android.text.style.TabStopSpan;
39 
40 import com.android.internal.annotations.VisibleForTesting;
41 import com.android.internal.util.ArrayUtils;
42 import com.android.internal.util.GrowingArrayUtils;
43 
44 import java.lang.annotation.Retention;
45 import java.lang.annotation.RetentionPolicy;
46 import java.util.Arrays;
47 import java.util.List;
48 
49 /**
50  * A base class that manages text layout in visual elements on
51  * the screen.
52  * <p>For text that will be edited, use a {@link DynamicLayout},
53  * which will be updated as the text changes.
54  * For text that will not change, use a {@link StaticLayout}.
55  */
56 public abstract class Layout {
57     /** @hide */
58     @IntDef(prefix = { "BREAK_STRATEGY_" }, value = {
59             LineBreaker.BREAK_STRATEGY_SIMPLE,
60             LineBreaker.BREAK_STRATEGY_HIGH_QUALITY,
61             LineBreaker.BREAK_STRATEGY_BALANCED
62     })
63     @Retention(RetentionPolicy.SOURCE)
64     public @interface BreakStrategy {}
65 
66     /**
67      * Value for break strategy indicating simple line breaking. Automatic hyphens are not added
68      * (though soft hyphens are respected), and modifying text generally doesn't affect the layout
69      * before it (which yields a more consistent user experience when editing), but layout may not
70      * be the highest quality.
71      */
72     public static final int BREAK_STRATEGY_SIMPLE = LineBreaker.BREAK_STRATEGY_SIMPLE;
73 
74     /**
75      * Value for break strategy indicating high quality line breaking, including automatic
76      * hyphenation and doing whole-paragraph optimization of line breaks.
77      */
78     public static final int BREAK_STRATEGY_HIGH_QUALITY = LineBreaker.BREAK_STRATEGY_HIGH_QUALITY;
79 
80     /**
81      * Value for break strategy indicating balanced line breaking. The breaks are chosen to
82      * make all lines as close to the same length as possible, including automatic hyphenation.
83      */
84     public static final int BREAK_STRATEGY_BALANCED = LineBreaker.BREAK_STRATEGY_BALANCED;
85 
86     /** @hide */
87     @IntDef(prefix = { "HYPHENATION_FREQUENCY_" }, value = {
88             HYPHENATION_FREQUENCY_NORMAL,
89             HYPHENATION_FREQUENCY_NORMAL_FAST,
90             HYPHENATION_FREQUENCY_FULL,
91             HYPHENATION_FREQUENCY_FULL_FAST,
92             HYPHENATION_FREQUENCY_NONE
93     })
94     @Retention(RetentionPolicy.SOURCE)
95     public @interface HyphenationFrequency {}
96 
97     /**
98      * Value for hyphenation frequency indicating no automatic hyphenation. Useful
99      * for backward compatibility, and for cases where the automatic hyphenation algorithm results
100      * in incorrect hyphenation. Mid-word breaks may still happen when a word is wider than the
101      * layout and there is otherwise no valid break. Soft hyphens are ignored and will not be used
102      * as suggestions for potential line breaks.
103      */
104     public static final int HYPHENATION_FREQUENCY_NONE = 0;
105 
106     /**
107      * Value for hyphenation frequency indicating a light amount of automatic hyphenation, which
108      * is a conservative default. Useful for informal cases, such as short sentences or chat
109      * messages.
110      */
111     public static final int HYPHENATION_FREQUENCY_NORMAL = 1;
112 
113     /**
114      * Value for hyphenation frequency indicating the full amount of automatic hyphenation, typical
115      * in typography. Useful for running text and where it's important to put the maximum amount of
116      * text in a screen with limited space.
117      */
118     public static final int HYPHENATION_FREQUENCY_FULL = 2;
119 
120     /**
121      * Value for hyphenation frequency indicating a light amount of automatic hyphenation with
122      * using faster algorithm.
123      *
124      * This option is useful for informal cases, such as short sentences or chat messages. To make
125      * text rendering faster with hyphenation, this algorithm ignores some hyphen character related
126      * typographic features, e.g. kerning.
127      */
128     public static final int HYPHENATION_FREQUENCY_NORMAL_FAST = 3;
129     /**
130      * Value for hyphenation frequency indicating the full amount of automatic hyphenation with
131      * using faster algorithm.
132      *
133      * This option is useful for running text and where it's important to put the maximum amount of
134      * text in a screen with limited space. To make text rendering faster with hyphenation, this
135      * algorithm ignores some hyphen character related typographic features, e.g. kerning.
136      */
137     public static final int HYPHENATION_FREQUENCY_FULL_FAST = 4;
138 
139     private static final ParagraphStyle[] NO_PARA_SPANS =
140         ArrayUtils.emptyArray(ParagraphStyle.class);
141 
142     /** @hide */
143     @IntDef(prefix = { "JUSTIFICATION_MODE_" }, value = {
144             LineBreaker.JUSTIFICATION_MODE_NONE,
145             LineBreaker.JUSTIFICATION_MODE_INTER_WORD
146     })
147     @Retention(RetentionPolicy.SOURCE)
148     public @interface JustificationMode {}
149 
150     /**
151      * Value for justification mode indicating no justification.
152      */
153     public static final int JUSTIFICATION_MODE_NONE = LineBreaker.JUSTIFICATION_MODE_NONE;
154 
155     /**
156      * Value for justification mode indicating the text is justified by stretching word spacing.
157      */
158     public static final int JUSTIFICATION_MODE_INTER_WORD =
159             LineBreaker.JUSTIFICATION_MODE_INTER_WORD;
160 
161     /*
162      * Line spacing multiplier for default line spacing.
163      */
164     public static final float DEFAULT_LINESPACING_MULTIPLIER = 1.0f;
165 
166     /*
167      * Line spacing addition for default line spacing.
168      */
169     public static final float DEFAULT_LINESPACING_ADDITION = 0.0f;
170 
171     /**
172      * Strategy which considers a text segment to be inside a rectangle area if the segment bounds
173      * intersect the rectangle.
174      */
175     @NonNull
176     public static final TextInclusionStrategy INCLUSION_STRATEGY_ANY_OVERLAP =
177             RectF::intersects;
178 
179     /**
180      * Strategy which considers a text segment to be inside a rectangle area if the center of the
181      * segment bounds is inside the rectangle.
182      */
183     @NonNull
184     public static final TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_CENTER =
185             (segmentBounds, area) ->
186                     area.contains(segmentBounds.centerX(), segmentBounds.centerY());
187 
188     /**
189      * Strategy which considers a text segment to be inside a rectangle area if the segment bounds
190      * are completely contained within the rectangle.
191      */
192     @NonNull
193     public static final TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_ALL =
194             (segmentBounds, area) -> area.contains(segmentBounds);
195 
196     /**
197      * Return how wide a layout must be in order to display the specified text with one line per
198      * paragraph.
199      *
200      * <p>As of O, Uses
201      * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR} as the default text direction heuristics. In
202      * the earlier versions uses {@link TextDirectionHeuristics#LTR} as the default.</p>
203      */
getDesiredWidth(CharSequence source, TextPaint paint)204     public static float getDesiredWidth(CharSequence source,
205                                         TextPaint paint) {
206         return getDesiredWidth(source, 0, source.length(), paint);
207     }
208 
209     /**
210      * Return how wide a layout must be in order to display the specified text slice with one
211      * line per paragraph.
212      *
213      * <p>As of O, Uses
214      * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR} as the default text direction heuristics. In
215      * the earlier versions uses {@link TextDirectionHeuristics#LTR} as the default.</p>
216      */
getDesiredWidth(CharSequence source, int start, int end, TextPaint paint)217     public static float getDesiredWidth(CharSequence source, int start, int end, TextPaint paint) {
218         return getDesiredWidth(source, start, end, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR);
219     }
220 
221     /**
222      * Return how wide a layout must be in order to display the
223      * specified text slice with one line per paragraph.
224      *
225      * @hide
226      */
getDesiredWidth(CharSequence source, int start, int end, TextPaint paint, TextDirectionHeuristic textDir)227     public static float getDesiredWidth(CharSequence source, int start, int end, TextPaint paint,
228             TextDirectionHeuristic textDir) {
229         return getDesiredWidthWithLimit(source, start, end, paint, textDir, Float.MAX_VALUE);
230     }
231     /**
232      * Return how wide a layout must be in order to display the
233      * specified text slice with one line per paragraph.
234      *
235      * If the measured width exceeds given limit, returns limit value instead.
236      * @hide
237      */
getDesiredWidthWithLimit(CharSequence source, int start, int end, TextPaint paint, TextDirectionHeuristic textDir, float upperLimit)238     public static float getDesiredWidthWithLimit(CharSequence source, int start, int end,
239             TextPaint paint, TextDirectionHeuristic textDir, float upperLimit) {
240         float need = 0;
241 
242         int next;
243         for (int i = start; i <= end; i = next) {
244             next = TextUtils.indexOf(source, '\n', i, end);
245 
246             if (next < 0)
247                 next = end;
248 
249             // note, omits trailing paragraph char
250             float w = measurePara(paint, source, i, next, textDir);
251             if (w > upperLimit) {
252                 return upperLimit;
253             }
254 
255             if (w > need)
256                 need = w;
257 
258             next++;
259         }
260 
261         return need;
262     }
263 
264     /**
265      * Subclasses of Layout use this constructor to set the display text,
266      * width, and other standard properties.
267      * @param text the text to render
268      * @param paint the default paint for the layout.  Styles can override
269      * various attributes of the paint.
270      * @param width the wrapping width for the text.
271      * @param align whether to left, right, or center the text.  Styles can
272      * override the alignment.
273      * @param spacingMult factor by which to scale the font size to get the
274      * default line spacing
275      * @param spacingAdd amount to add to the default line spacing
276      */
Layout(CharSequence text, TextPaint paint, int width, Alignment align, float spacingMult, float spacingAdd)277     protected Layout(CharSequence text, TextPaint paint,
278                      int width, Alignment align,
279                      float spacingMult, float spacingAdd) {
280         this(text, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
281                 spacingMult, spacingAdd);
282     }
283 
284     /**
285      * Subclasses of Layout use this constructor to set the display text,
286      * width, and other standard properties.
287      * @param text the text to render
288      * @param paint the default paint for the layout.  Styles can override
289      * various attributes of the paint.
290      * @param width the wrapping width for the text.
291      * @param align whether to left, right, or center the text.  Styles can
292      * override the alignment.
293      * @param spacingMult factor by which to scale the font size to get the
294      * default line spacing
295      * @param spacingAdd amount to add to the default line spacing
296      *
297      * @hide
298      */
Layout(CharSequence text, TextPaint paint, int width, Alignment align, TextDirectionHeuristic textDir, float spacingMult, float spacingAdd)299     protected Layout(CharSequence text, TextPaint paint,
300                      int width, Alignment align, TextDirectionHeuristic textDir,
301                      float spacingMult, float spacingAdd) {
302 
303         if (width < 0)
304             throw new IllegalArgumentException("Layout: " + width + " < 0");
305 
306         // Ensure paint doesn't have baselineShift set.
307         // While normally we don't modify the paint the user passed in,
308         // we were already doing this in Styled.drawUniformRun with both
309         // baselineShift and bgColor.  We probably should reevaluate bgColor.
310         if (paint != null) {
311             paint.bgColor = 0;
312             paint.baselineShift = 0;
313         }
314 
315         mText = text;
316         mPaint = paint;
317         mWidth = width;
318         mAlignment = align;
319         mSpacingMult = spacingMult;
320         mSpacingAdd = spacingAdd;
321         mSpannedText = text instanceof Spanned;
322         mTextDir = textDir;
323     }
324 
325     /** @hide */
setJustificationMode(@ustificationMode int justificationMode)326     protected void setJustificationMode(@JustificationMode int justificationMode) {
327         mJustificationMode = justificationMode;
328     }
329 
330     /**
331      * Replace constructor properties of this Layout with new ones.  Be careful.
332      */
replaceWith(CharSequence text, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd)333     /* package */ void replaceWith(CharSequence text, TextPaint paint,
334                               int width, Alignment align,
335                               float spacingmult, float spacingadd) {
336         if (width < 0) {
337             throw new IllegalArgumentException("Layout: " + width + " < 0");
338         }
339 
340         mText = text;
341         mPaint = paint;
342         mWidth = width;
343         mAlignment = align;
344         mSpacingMult = spacingmult;
345         mSpacingAdd = spacingadd;
346         mSpannedText = text instanceof Spanned;
347     }
348 
349     /**
350      * Draw this Layout on the specified Canvas.
351      *
352      * This API draws background first, then draws text on top of it.
353      *
354      * @see #draw(Canvas, List, List, Path, Paint, int)
355      */
draw(Canvas c)356     public void draw(Canvas c) {
357         draw(c, (Path) null, (Paint) null, 0);
358     }
359 
360     /**
361      * Draw this Layout on the specified canvas, with the highlight path drawn
362      * between the background and the text.
363      *
364      * @param canvas the canvas
365      * @param selectionHighlight the path of the selection highlight or cursor; can be null
366      * @param selectionHighlightPaint the paint for the selection highlight
367      * @param cursorOffsetVertical the amount to temporarily translate the
368      *        canvas while rendering the highlight
369      *
370      * @see #draw(Canvas, List, List, Path, Paint, int)
371      */
draw( Canvas canvas, Path selectionHighlight, Paint selectionHighlightPaint, int cursorOffsetVertical)372     public void draw(
373             Canvas canvas, Path selectionHighlight,
374             Paint selectionHighlightPaint, int cursorOffsetVertical) {
375         draw(canvas, null, null, selectionHighlight, selectionHighlightPaint, cursorOffsetVertical);
376     }
377 
378     /**
379      * Draw this layout on the specified canvas.
380      *
381      * This API draws background first, then draws highlight paths on top of it, then draws
382      * selection or cursor, then finally draws text on top of it.
383      *
384      * @see #drawBackground(Canvas)
385      * @see #drawText(Canvas)
386      *
387      * @param canvas the canvas
388      * @param highlightPaths the path of the highlights. The highlightPaths and highlightPaints must
389      *                      have the same length and aligned in the same order. For example, the
390      *                      paint of the n-th of the highlightPaths should be stored at the n-th of
391      *                      highlightPaints.
392      * @param highlightPaints the paints for the highlights. The highlightPaths and highlightPaints
393      *                        must have the same length and aligned in the same order. For example,
394      *                        the paint of the n-th of the highlightPaths should be stored at the
395      *                        n-th of highlightPaints.
396      * @param selectionPath the selection or cursor path
397      * @param selectionPaint the paint for the selection or cursor.
398      * @param cursorOffsetVertical the amount to temporarily translate the canvas while rendering
399      *                            the highlight
400      */
draw(@onNull Canvas canvas, @Nullable List<Path> highlightPaths, @Nullable List<Paint> highlightPaints, @Nullable Path selectionPath, @Nullable Paint selectionPaint, int cursorOffsetVertical)401     public void draw(@NonNull Canvas canvas,
402             @Nullable List<Path> highlightPaths,
403             @Nullable List<Paint> highlightPaints,
404             @Nullable Path selectionPath,
405             @Nullable Paint selectionPaint,
406             int cursorOffsetVertical) {
407         final long lineRange = getLineRangeForDraw(canvas);
408         int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
409         int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
410         if (lastLine < 0) return;
411 
412         drawWithoutText(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint,
413                 cursorOffsetVertical, firstLine, lastLine);
414         drawText(canvas, firstLine, lastLine);
415     }
416 
417     /**
418      * Draw text part of this layout.
419      *
420      * Different from {@link #draw(Canvas, List, List, Path, Paint, int)} API, this API only draws
421      * text part, not drawing highlights, selections, or backgrounds.
422      *
423      * @see #draw(Canvas, List, List, Path, Paint, int)
424      * @see #drawBackground(Canvas)
425      *
426      * @param canvas the canvas
427      */
drawText(@onNull Canvas canvas)428     public void drawText(@NonNull Canvas canvas) {
429         final long lineRange = getLineRangeForDraw(canvas);
430         int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
431         int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
432         if (lastLine < 0) return;
433         drawText(canvas, firstLine, lastLine);
434     }
435 
436     /**
437      * Draw background of this layout.
438      *
439      * Different from {@link #draw(Canvas, List, List, Path, Paint, int)} API, this API only draws
440      * background, not drawing text, highlights or selections. The background here is drawn by
441      * {@link LineBackgroundSpan} attached to the text.
442      *
443      * @see #draw(Canvas, List, List, Path, Paint, int)
444      * @see #drawText(Canvas)
445      *
446      * @param canvas the canvas
447      */
drawBackground(@onNull Canvas canvas)448     public void drawBackground(@NonNull Canvas canvas) {
449         final long lineRange = getLineRangeForDraw(canvas);
450         int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
451         int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
452         if (lastLine < 0) return;
453         drawBackground(canvas, firstLine, lastLine);
454     }
455 
456     /**
457      * @hide public for Editor.java
458      */
drawWithoutText( @onNull Canvas canvas, @Nullable List<Path> highlightPaths, @Nullable List<Paint> highlightPaints, @Nullable Path selectionPath, @Nullable Paint selectionPaint, int cursorOffsetVertical, int firstLine, int lastLine)459     public void drawWithoutText(
460             @NonNull Canvas canvas,
461             @Nullable List<Path> highlightPaths,
462             @Nullable List<Paint> highlightPaints,
463             @Nullable Path selectionPath,
464             @Nullable Paint selectionPaint,
465             int cursorOffsetVertical,
466             int firstLine,
467             int lastLine) {
468         drawBackground(canvas, firstLine, lastLine);
469         if (highlightPaths == null && highlightPaints == null) {
470             return;
471         }
472         if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical);
473         try {
474             if (highlightPaths != null) {
475                 if (highlightPaints == null) {
476                     throw new IllegalArgumentException(
477                             "if highlight is specified, highlightPaint must be specified.");
478                 }
479                 if (highlightPaints.size() != highlightPaths.size()) {
480                     throw new IllegalArgumentException(
481                             "The highlight path size is different from the size of highlight"
482                                     + " paints");
483                 }
484                 for (int i = 0; i < highlightPaths.size(); ++i) {
485                     final Path highlight = highlightPaths.get(i);
486                     final Paint highlightPaint = highlightPaints.get(i);
487                     if (highlight != null) {
488                         canvas.drawPath(highlight, highlightPaint);
489                     }
490                 }
491             }
492 
493             if (selectionPath != null) {
494                 canvas.drawPath(selectionPath, selectionPaint);
495             }
496         } finally {
497             if (cursorOffsetVertical != 0) canvas.translate(0, -cursorOffsetVertical);
498         }
499     }
500 
isJustificationRequired(int lineNum)501     private boolean isJustificationRequired(int lineNum) {
502         if (mJustificationMode == JUSTIFICATION_MODE_NONE) return false;
503         final int lineEnd = getLineEnd(lineNum);
504         return lineEnd < mText.length() && mText.charAt(lineEnd - 1) != '\n';
505     }
506 
getJustifyWidth(int lineNum)507     private float getJustifyWidth(int lineNum) {
508         Alignment paraAlign = mAlignment;
509 
510         int left = 0;
511         int right = mWidth;
512 
513         final int dir = getParagraphDirection(lineNum);
514 
515         ParagraphStyle[] spans = NO_PARA_SPANS;
516         if (mSpannedText) {
517             Spanned sp = (Spanned) mText;
518             final int start = getLineStart(lineNum);
519 
520             final boolean isFirstParaLine = (start == 0 || mText.charAt(start - 1) == '\n');
521 
522             if (isFirstParaLine) {
523                 final int spanEnd = sp.nextSpanTransition(start, mText.length(),
524                         ParagraphStyle.class);
525                 spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);
526 
527                 for (int n = spans.length - 1; n >= 0; n--) {
528                     if (spans[n] instanceof AlignmentSpan) {
529                         paraAlign = ((AlignmentSpan) spans[n]).getAlignment();
530                         break;
531                     }
532                 }
533             }
534 
535             final int length = spans.length;
536             boolean useFirstLineMargin = isFirstParaLine;
537             for (int n = 0; n < length; n++) {
538                 if (spans[n] instanceof LeadingMarginSpan2) {
539                     int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount();
540                     int startLine = getLineForOffset(sp.getSpanStart(spans[n]));
541                     if (lineNum < startLine + count) {
542                         useFirstLineMargin = true;
543                         break;
544                     }
545                 }
546             }
547             for (int n = 0; n < length; n++) {
548                 if (spans[n] instanceof LeadingMarginSpan) {
549                     LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
550                     if (dir == DIR_RIGHT_TO_LEFT) {
551                         right -= margin.getLeadingMargin(useFirstLineMargin);
552                     } else {
553                         left += margin.getLeadingMargin(useFirstLineMargin);
554                     }
555                 }
556             }
557         }
558 
559         final Alignment align;
560         if (paraAlign == Alignment.ALIGN_LEFT) {
561             align = (dir == DIR_LEFT_TO_RIGHT) ?  Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
562         } else if (paraAlign == Alignment.ALIGN_RIGHT) {
563             align = (dir == DIR_LEFT_TO_RIGHT) ?  Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
564         } else {
565             align = paraAlign;
566         }
567 
568         final int indentWidth;
569         if (align == Alignment.ALIGN_NORMAL) {
570             if (dir == DIR_LEFT_TO_RIGHT) {
571                 indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
572             } else {
573                 indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
574             }
575         } else if (align == Alignment.ALIGN_OPPOSITE) {
576             if (dir == DIR_LEFT_TO_RIGHT) {
577                 indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
578             } else {
579                 indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
580             }
581         } else { // Alignment.ALIGN_CENTER
582             indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
583         }
584 
585         return right - left - indentWidth;
586     }
587 
588     /**
589      * @hide
590      */
591     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
drawText(Canvas canvas, int firstLine, int lastLine)592     public void drawText(Canvas canvas, int firstLine, int lastLine) {
593         int previousLineBottom = getLineTop(firstLine);
594         int previousLineEnd = getLineStart(firstLine);
595         ParagraphStyle[] spans = NO_PARA_SPANS;
596         int spanEnd = 0;
597         final TextPaint paint = mWorkPaint;
598         paint.set(mPaint);
599         CharSequence buf = mText;
600 
601         Alignment paraAlign = mAlignment;
602         TabStops tabStops = null;
603         boolean tabStopsIsInitialized = false;
604 
605         TextLine tl = TextLine.obtain();
606 
607         // Draw the lines, one at a time.
608         // The baseline is the top of the following line minus the current line's descent.
609         for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {
610             int start = previousLineEnd;
611             previousLineEnd = getLineStart(lineNum + 1);
612             final boolean justify = isJustificationRequired(lineNum);
613             int end = getLineVisibleEnd(lineNum, start, previousLineEnd);
614             paint.setStartHyphenEdit(getStartHyphenEdit(lineNum));
615             paint.setEndHyphenEdit(getEndHyphenEdit(lineNum));
616 
617             int ltop = previousLineBottom;
618             int lbottom = getLineTop(lineNum + 1);
619             previousLineBottom = lbottom;
620             int lbaseline = lbottom - getLineDescent(lineNum);
621 
622             int dir = getParagraphDirection(lineNum);
623             int left = 0;
624             int right = mWidth;
625 
626             if (mSpannedText) {
627                 Spanned sp = (Spanned) buf;
628                 int textLength = buf.length();
629                 boolean isFirstParaLine = (start == 0 || buf.charAt(start - 1) == '\n');
630 
631                 // New batch of paragraph styles, collect into spans array.
632                 // Compute the alignment, last alignment style wins.
633                 // Reset tabStops, we'll rebuild if we encounter a line with
634                 // tabs.
635                 // We expect paragraph spans to be relatively infrequent, use
636                 // spanEnd so that we can check less frequently.  Since
637                 // paragraph styles ought to apply to entire paragraphs, we can
638                 // just collect the ones present at the start of the paragraph.
639                 // If spanEnd is before the end of the paragraph, that's not
640                 // our problem.
641                 if (start >= spanEnd && (lineNum == firstLine || isFirstParaLine)) {
642                     spanEnd = sp.nextSpanTransition(start, textLength,
643                                                     ParagraphStyle.class);
644                     spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);
645 
646                     paraAlign = mAlignment;
647                     for (int n = spans.length - 1; n >= 0; n--) {
648                         if (spans[n] instanceof AlignmentSpan) {
649                             paraAlign = ((AlignmentSpan) spans[n]).getAlignment();
650                             break;
651                         }
652                     }
653 
654                     tabStopsIsInitialized = false;
655                 }
656 
657                 // Draw all leading margin spans.  Adjust left or right according
658                 // to the paragraph direction of the line.
659                 final int length = spans.length;
660                 boolean useFirstLineMargin = isFirstParaLine;
661                 for (int n = 0; n < length; n++) {
662                     if (spans[n] instanceof LeadingMarginSpan2) {
663                         int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount();
664                         int startLine = getLineForOffset(sp.getSpanStart(spans[n]));
665                         // if there is more than one LeadingMarginSpan2, use
666                         // the count that is greatest
667                         if (lineNum < startLine + count) {
668                             useFirstLineMargin = true;
669                             break;
670                         }
671                     }
672                 }
673                 for (int n = 0; n < length; n++) {
674                     if (spans[n] instanceof LeadingMarginSpan) {
675                         LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
676                         if (dir == DIR_RIGHT_TO_LEFT) {
677                             margin.drawLeadingMargin(canvas, paint, right, dir, ltop,
678                                                      lbaseline, lbottom, buf,
679                                                      start, end, isFirstParaLine, this);
680                             right -= margin.getLeadingMargin(useFirstLineMargin);
681                         } else {
682                             margin.drawLeadingMargin(canvas, paint, left, dir, ltop,
683                                                      lbaseline, lbottom, buf,
684                                                      start, end, isFirstParaLine, this);
685                             left += margin.getLeadingMargin(useFirstLineMargin);
686                         }
687                     }
688                 }
689             }
690 
691             boolean hasTab = getLineContainsTab(lineNum);
692             // Can't tell if we have tabs for sure, currently
693             if (hasTab && !tabStopsIsInitialized) {
694                 if (tabStops == null) {
695                     tabStops = new TabStops(TAB_INCREMENT, spans);
696                 } else {
697                     tabStops.reset(TAB_INCREMENT, spans);
698                 }
699                 tabStopsIsInitialized = true;
700             }
701 
702             // Determine whether the line aligns to normal, opposite, or center.
703             Alignment align = paraAlign;
704             if (align == Alignment.ALIGN_LEFT) {
705                 align = (dir == DIR_LEFT_TO_RIGHT) ?
706                     Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
707             } else if (align == Alignment.ALIGN_RIGHT) {
708                 align = (dir == DIR_LEFT_TO_RIGHT) ?
709                     Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
710             }
711 
712             int x;
713             final int indentWidth;
714             if (align == Alignment.ALIGN_NORMAL) {
715                 if (dir == DIR_LEFT_TO_RIGHT) {
716                     indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
717                     x = left + indentWidth;
718                 } else {
719                     indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
720                     x = right - indentWidth;
721                 }
722             } else {
723                 int max = (int)getLineExtent(lineNum, tabStops, false);
724                 if (align == Alignment.ALIGN_OPPOSITE) {
725                     if (dir == DIR_LEFT_TO_RIGHT) {
726                         indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
727                         x = right - max - indentWidth;
728                     } else {
729                         indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
730                         x = left - max + indentWidth;
731                     }
732                 } else { // Alignment.ALIGN_CENTER
733                     indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
734                     max = max & ~1;
735                     x = ((right + left - max) >> 1) + indentWidth;
736                 }
737             }
738 
739             Directions directions = getLineDirections(lineNum);
740             if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab && !justify) {
741                 // XXX: assumes there's nothing additional to be done
742                 canvas.drawText(buf, start, end, x, lbaseline, paint);
743             } else {
744                 tl.set(paint, buf, start, end, dir, directions, hasTab, tabStops,
745                         getEllipsisStart(lineNum),
746                         getEllipsisStart(lineNum) + getEllipsisCount(lineNum),
747                         isFallbackLineSpacingEnabled());
748                 if (justify) {
749                     tl.justify(right - left - indentWidth);
750                 }
751                 tl.draw(canvas, x, ltop, lbaseline, lbottom);
752             }
753         }
754 
755         TextLine.recycle(tl);
756     }
757 
758     /**
759      * @hide
760      */
761     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
drawBackground( @onNull Canvas canvas, int firstLine, int lastLine)762     public void drawBackground(
763             @NonNull Canvas canvas,
764             int firstLine, int lastLine) {
765         // First, draw LineBackgroundSpans.
766         // LineBackgroundSpans know nothing about the alignment, margins, or
767         // direction of the layout or line.  XXX: Should they?
768         // They are evaluated at each line.
769         if (mSpannedText) {
770             if (mLineBackgroundSpans == null) {
771                 mLineBackgroundSpans = new SpanSet<LineBackgroundSpan>(LineBackgroundSpan.class);
772             }
773 
774             Spanned buffer = (Spanned) mText;
775             int textLength = buffer.length();
776             mLineBackgroundSpans.init(buffer, 0, textLength);
777 
778             if (mLineBackgroundSpans.numberOfSpans > 0) {
779                 int previousLineBottom = getLineTop(firstLine);
780                 int previousLineEnd = getLineStart(firstLine);
781                 ParagraphStyle[] spans = NO_PARA_SPANS;
782                 int spansLength = 0;
783                 TextPaint paint = mPaint;
784                 int spanEnd = 0;
785                 final int width = mWidth;
786                 for (int i = firstLine; i <= lastLine; i++) {
787                     int start = previousLineEnd;
788                     int end = getLineStart(i + 1);
789                     previousLineEnd = end;
790 
791                     int ltop = previousLineBottom;
792                     int lbottom = getLineTop(i + 1);
793                     previousLineBottom = lbottom;
794                     int lbaseline = lbottom - getLineDescent(i);
795 
796                     if (end >= spanEnd) {
797                         // These should be infrequent, so we'll use this so that
798                         // we don't have to check as often.
799                         spanEnd = mLineBackgroundSpans.getNextTransition(start, textLength);
800                         // All LineBackgroundSpans on a line contribute to its background.
801                         spansLength = 0;
802                         // Duplication of the logic of getParagraphSpans
803                         if (start != end || start == 0) {
804                             // Equivalent to a getSpans(start, end), but filling the 'spans' local
805                             // array instead to reduce memory allocation
806                             for (int j = 0; j < mLineBackgroundSpans.numberOfSpans; j++) {
807                                 // equal test is valid since both intervals are not empty by
808                                 // construction
809                                 if (mLineBackgroundSpans.spanStarts[j] >= end ||
810                                         mLineBackgroundSpans.spanEnds[j] <= start) continue;
811                                 spans = GrowingArrayUtils.append(
812                                         spans, spansLength, mLineBackgroundSpans.spans[j]);
813                                 spansLength++;
814                             }
815                         }
816                     }
817 
818                     for (int n = 0; n < spansLength; n++) {
819                         LineBackgroundSpan lineBackgroundSpan = (LineBackgroundSpan) spans[n];
820                         lineBackgroundSpan.drawBackground(canvas, paint, 0, width,
821                                 ltop, lbaseline, lbottom,
822                                 buffer, start, end, i);
823                     }
824                 }
825             }
826             mLineBackgroundSpans.recycle();
827         }
828     }
829 
830     /**
831      * @param canvas
832      * @return The range of lines that need to be drawn, possibly empty.
833      * @hide
834      */
835     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getLineRangeForDraw(Canvas canvas)836     public long getLineRangeForDraw(Canvas canvas) {
837         int dtop, dbottom;
838 
839         synchronized (sTempRect) {
840             if (!canvas.getClipBounds(sTempRect)) {
841                 // Negative range end used as a special flag
842                 return TextUtils.packRangeInLong(0, -1);
843             }
844 
845             dtop = sTempRect.top;
846             dbottom = sTempRect.bottom;
847         }
848 
849         final int top = Math.max(dtop, 0);
850         final int bottom = Math.min(getLineTop(getLineCount()), dbottom);
851 
852         if (top >= bottom) return TextUtils.packRangeInLong(0, -1);
853         return TextUtils.packRangeInLong(getLineForVertical(top), getLineForVertical(bottom));
854     }
855 
856     /**
857      * Return the start position of the line, given the left and right bounds of the margins.
858      *
859      * @param line the line index
860      * @param left the left bounds (0, or leading margin if ltr para)
861      * @param right the right bounds (width, minus leading margin if rtl para)
862      * @return the start position of the line (to right of line if rtl para)
863      */
getLineStartPos(int line, int left, int right)864     private int getLineStartPos(int line, int left, int right) {
865         // Adjust the point at which to start rendering depending on the
866         // alignment of the paragraph.
867         Alignment align = getParagraphAlignment(line);
868         int dir = getParagraphDirection(line);
869 
870         if (align == Alignment.ALIGN_LEFT) {
871             align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
872         } else if (align == Alignment.ALIGN_RIGHT) {
873             align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
874         }
875 
876         int x;
877         if (align == Alignment.ALIGN_NORMAL) {
878             if (dir == DIR_LEFT_TO_RIGHT) {
879                 x = left + getIndentAdjust(line, Alignment.ALIGN_LEFT);
880             } else {
881                 x = right + getIndentAdjust(line, Alignment.ALIGN_RIGHT);
882             }
883         } else {
884             TabStops tabStops = null;
885             if (mSpannedText && getLineContainsTab(line)) {
886                 Spanned spanned = (Spanned) mText;
887                 int start = getLineStart(line);
888                 int spanEnd = spanned.nextSpanTransition(start, spanned.length(),
889                         TabStopSpan.class);
890                 TabStopSpan[] tabSpans = getParagraphSpans(spanned, start, spanEnd,
891                         TabStopSpan.class);
892                 if (tabSpans.length > 0) {
893                     tabStops = new TabStops(TAB_INCREMENT, tabSpans);
894                 }
895             }
896             int max = (int)getLineExtent(line, tabStops, false);
897             if (align == Alignment.ALIGN_OPPOSITE) {
898                 if (dir == DIR_LEFT_TO_RIGHT) {
899                     x = right - max + getIndentAdjust(line, Alignment.ALIGN_RIGHT);
900                 } else {
901                     // max is negative here
902                     x = left - max + getIndentAdjust(line, Alignment.ALIGN_LEFT);
903                 }
904             } else { // Alignment.ALIGN_CENTER
905                 max = max & ~1;
906                 x = (left + right - max) >> 1 + getIndentAdjust(line, Alignment.ALIGN_CENTER);
907             }
908         }
909         return x;
910     }
911 
912     /**
913      * Return the text that is displayed by this Layout.
914      */
getText()915     public final CharSequence getText() {
916         return mText;
917     }
918 
919     /**
920      * Return the base Paint properties for this layout.
921      * Do NOT change the paint, which may result in funny
922      * drawing for this layout.
923      */
getPaint()924     public final TextPaint getPaint() {
925         return mPaint;
926     }
927 
928     /**
929      * Return the width of this layout.
930      */
getWidth()931     public final int getWidth() {
932         return mWidth;
933     }
934 
935     /**
936      * Return the width to which this Layout is ellipsizing, or
937      * {@link #getWidth} if it is not doing anything special.
938      */
getEllipsizedWidth()939     public int getEllipsizedWidth() {
940         return mWidth;
941     }
942 
943     /**
944      * Increase the width of this layout to the specified width.
945      * Be careful to use this only when you know it is appropriate&mdash;
946      * it does not cause the text to reflow to use the full new width.
947      */
increaseWidthTo(int wid)948     public final void increaseWidthTo(int wid) {
949         if (wid < mWidth) {
950             throw new RuntimeException("attempted to reduce Layout width");
951         }
952 
953         mWidth = wid;
954     }
955 
956     /**
957      * Return the total height of this layout.
958      */
getHeight()959     public int getHeight() {
960         return getLineTop(getLineCount());
961     }
962 
963     /**
964      * Return the total height of this layout.
965      *
966      * @param cap if true and max lines is set, returns the height of the layout at the max lines.
967      *
968      * @hide
969      */
getHeight(boolean cap)970     public int getHeight(boolean cap) {
971         return getHeight();
972     }
973 
974     /**
975      * Return the base alignment of this layout.
976      */
getAlignment()977     public final Alignment getAlignment() {
978         return mAlignment;
979     }
980 
981     /**
982      * Return what the text height is multiplied by to get the line height.
983      */
getSpacingMultiplier()984     public final float getSpacingMultiplier() {
985         return mSpacingMult;
986     }
987 
988     /**
989      * Return the number of units of leading that are added to each line.
990      */
getSpacingAdd()991     public final float getSpacingAdd() {
992         return mSpacingAdd;
993     }
994 
995     /**
996      * Return the heuristic used to determine paragraph text direction.
997      * @hide
998      */
getTextDirectionHeuristic()999     public final TextDirectionHeuristic getTextDirectionHeuristic() {
1000         return mTextDir;
1001     }
1002 
1003     /**
1004      * Return the number of lines of text in this layout.
1005      */
getLineCount()1006     public abstract int getLineCount();
1007 
1008     /**
1009      * Return the baseline for the specified line (0&hellip;getLineCount() - 1)
1010      * If bounds is not null, return the top, left, right, bottom extents
1011      * of the specified line in it.
1012      * @param line which line to examine (0..getLineCount() - 1)
1013      * @param bounds Optional. If not null, it returns the extent of the line
1014      * @return the Y-coordinate of the baseline
1015      */
getLineBounds(int line, Rect bounds)1016     public int getLineBounds(int line, Rect bounds) {
1017         if (bounds != null) {
1018             bounds.left = 0;     // ???
1019             bounds.top = getLineTop(line);
1020             bounds.right = mWidth;   // ???
1021             bounds.bottom = getLineTop(line + 1);
1022         }
1023         return getLineBaseline(line);
1024     }
1025 
1026     /**
1027      * Return the vertical position of the top of the specified line
1028      * (0&hellip;getLineCount()).
1029      * If the specified line is equal to the line count, returns the
1030      * bottom of the last line.
1031      */
getLineTop(int line)1032     public abstract int getLineTop(int line);
1033 
1034     /**
1035      * Return the descent of the specified line(0&hellip;getLineCount() - 1).
1036      */
getLineDescent(int line)1037     public abstract int getLineDescent(int line);
1038 
1039     /**
1040      * Return the text offset of the beginning of the specified line (
1041      * 0&hellip;getLineCount()). If the specified line is equal to the line
1042      * count, returns the length of the text.
1043      */
getLineStart(int line)1044     public abstract int getLineStart(int line);
1045 
1046     /**
1047      * Returns the primary directionality of the paragraph containing the
1048      * specified line, either 1 for left-to-right lines, or -1 for right-to-left
1049      * lines (see {@link #DIR_LEFT_TO_RIGHT}, {@link #DIR_RIGHT_TO_LEFT}).
1050      */
getParagraphDirection(int line)1051     public abstract int getParagraphDirection(int line);
1052 
1053     /**
1054      * Returns whether the specified line contains one or more
1055      * characters that need to be handled specially, like tabs.
1056      */
getLineContainsTab(int line)1057     public abstract boolean getLineContainsTab(int line);
1058 
1059     /**
1060      * Returns the directional run information for the specified line.
1061      * The array alternates counts of characters in left-to-right
1062      * and right-to-left segments of the line.
1063      *
1064      * <p>NOTE: this is inadequate to support bidirectional text, and will change.
1065      */
getLineDirections(int line)1066     public abstract Directions getLineDirections(int line);
1067 
1068     /**
1069      * Returns the (negative) number of extra pixels of ascent padding in the
1070      * top line of the Layout.
1071      */
getTopPadding()1072     public abstract int getTopPadding();
1073 
1074     /**
1075      * Returns the number of extra pixels of descent padding in the
1076      * bottom line of the Layout.
1077      */
getBottomPadding()1078     public abstract int getBottomPadding();
1079 
1080     /**
1081      * Returns the start hyphen edit for a line.
1082      *
1083      * @hide
1084      */
getStartHyphenEdit(int line)1085     public @Paint.StartHyphenEdit int getStartHyphenEdit(int line) {
1086         return Paint.START_HYPHEN_EDIT_NO_EDIT;
1087     }
1088 
1089     /**
1090      * Returns the end hyphen edit for a line.
1091      *
1092      * @hide
1093      */
getEndHyphenEdit(int line)1094     public @Paint.EndHyphenEdit int getEndHyphenEdit(int line) {
1095         return Paint.END_HYPHEN_EDIT_NO_EDIT;
1096     }
1097 
1098     /**
1099      * Returns the left indent for a line.
1100      *
1101      * @hide
1102      */
getIndentAdjust(int line, Alignment alignment)1103     public int getIndentAdjust(int line, Alignment alignment) {
1104         return 0;
1105     }
1106 
1107     /**
1108      * Return true if the fallback line space is enabled in this Layout.
1109      *
1110      * @return true if the fallback line space is enabled. Otherwise returns false.
1111      */
isFallbackLineSpacingEnabled()1112     public boolean isFallbackLineSpacingEnabled() {
1113         return false;
1114     }
1115 
1116     /**
1117      * Returns true if the character at offset and the preceding character
1118      * are at different run levels (and thus there's a split caret).
1119      * @param offset the offset
1120      * @return true if at a level boundary
1121      * @hide
1122      */
1123     @UnsupportedAppUsage
isLevelBoundary(int offset)1124     public boolean isLevelBoundary(int offset) {
1125         int line = getLineForOffset(offset);
1126         Directions dirs = getLineDirections(line);
1127         if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) {
1128             return false;
1129         }
1130 
1131         int[] runs = dirs.mDirections;
1132         int lineStart = getLineStart(line);
1133         int lineEnd = getLineEnd(line);
1134         if (offset == lineStart || offset == lineEnd) {
1135             int paraLevel = getParagraphDirection(line) == 1 ? 0 : 1;
1136             int runIndex = offset == lineStart ? 0 : runs.length - 2;
1137             return ((runs[runIndex + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK) != paraLevel;
1138         }
1139 
1140         offset -= lineStart;
1141         for (int i = 0; i < runs.length; i += 2) {
1142             if (offset == runs[i]) {
1143                 return true;
1144             }
1145         }
1146         return false;
1147     }
1148 
1149     /**
1150      * Returns true if the character at offset is right to left (RTL).
1151      * @param offset the offset
1152      * @return true if the character is RTL, false if it is LTR
1153      */
isRtlCharAt(int offset)1154     public boolean isRtlCharAt(int offset) {
1155         int line = getLineForOffset(offset);
1156         Directions dirs = getLineDirections(line);
1157         if (dirs == DIRS_ALL_LEFT_TO_RIGHT) {
1158             return false;
1159         }
1160         if (dirs == DIRS_ALL_RIGHT_TO_LEFT) {
1161             return  true;
1162         }
1163         int[] runs = dirs.mDirections;
1164         int lineStart = getLineStart(line);
1165         for (int i = 0; i < runs.length; i += 2) {
1166             int start = lineStart + runs[i];
1167             int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
1168             if (offset >= start && offset < limit) {
1169                 int level = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
1170                 return ((level & 1) != 0);
1171             }
1172         }
1173         // Should happen only if the offset is "out of bounds"
1174         return false;
1175     }
1176 
1177     /**
1178      * Returns the range of the run that the character at offset belongs to.
1179      * @param offset the offset
1180      * @return The range of the run
1181      * @hide
1182      */
getRunRange(int offset)1183     public long getRunRange(int offset) {
1184         int line = getLineForOffset(offset);
1185         Directions dirs = getLineDirections(line);
1186         if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) {
1187             return TextUtils.packRangeInLong(0, getLineEnd(line));
1188         }
1189         int[] runs = dirs.mDirections;
1190         int lineStart = getLineStart(line);
1191         for (int i = 0; i < runs.length; i += 2) {
1192             int start = lineStart + runs[i];
1193             int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
1194             if (offset >= start && offset < limit) {
1195                 return TextUtils.packRangeInLong(start, limit);
1196             }
1197         }
1198         // Should happen only if the offset is "out of bounds"
1199         return TextUtils.packRangeInLong(0, getLineEnd(line));
1200     }
1201 
1202     /**
1203      * Checks if the trailing BiDi level should be used for an offset
1204      *
1205      * This method is useful when the offset is at the BiDi level transition point and determine
1206      * which run need to be used. For example, let's think about following input: (L* denotes
1207      * Left-to-Right characters, R* denotes Right-to-Left characters.)
1208      * Input (Logical Order): L1 L2 L3 R1 R2 R3 L4 L5 L6
1209      * Input (Display Order): L1 L2 L3 R3 R2 R1 L4 L5 L6
1210      *
1211      * Then, think about selecting the range (3, 6). The offset=3 and offset=6 are ambiguous here
1212      * since they are at the BiDi transition point.  In Android, the offset is considered to be
1213      * associated with the trailing run if the BiDi level of the trailing run is higher than of the
1214      * previous run.  In this case, the BiDi level of the input text is as follows:
1215      *
1216      * Input (Logical Order): L1 L2 L3 R1 R2 R3 L4 L5 L6
1217      *              BiDi Run: [ Run 0 ][ Run 1 ][ Run 2 ]
1218      *            BiDi Level:  0  0  0  1  1  1  0  0  0
1219      *
1220      * Thus, offset = 3 is part of Run 1 and this method returns true for offset = 3, since the BiDi
1221      * level of Run 1 is higher than the level of Run 0.  Similarly, the offset = 6 is a part of Run
1222      * 1 and this method returns false for the offset = 6 since the BiDi level of Run 1 is higher
1223      * than the level of Run 2.
1224      *
1225      * @returns true if offset is at the BiDi level transition point and trailing BiDi level is
1226      *          higher than previous BiDi level. See above for the detail.
1227      * @hide
1228      */
1229     @VisibleForTesting
primaryIsTrailingPrevious(int offset)1230     public boolean primaryIsTrailingPrevious(int offset) {
1231         int line = getLineForOffset(offset);
1232         int lineStart = getLineStart(line);
1233         int lineEnd = getLineEnd(line);
1234         int[] runs = getLineDirections(line).mDirections;
1235 
1236         int levelAt = -1;
1237         for (int i = 0; i < runs.length; i += 2) {
1238             int start = lineStart + runs[i];
1239             int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
1240             if (limit > lineEnd) {
1241                 limit = lineEnd;
1242             }
1243             if (offset >= start && offset < limit) {
1244                 if (offset > start) {
1245                     // Previous character is at same level, so don't use trailing.
1246                     return false;
1247                 }
1248                 levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
1249                 break;
1250             }
1251         }
1252         if (levelAt == -1) {
1253             // Offset was limit of line.
1254             levelAt = getParagraphDirection(line) == 1 ? 0 : 1;
1255         }
1256 
1257         // At level boundary, check previous level.
1258         int levelBefore = -1;
1259         if (offset == lineStart) {
1260             levelBefore = getParagraphDirection(line) == 1 ? 0 : 1;
1261         } else {
1262             offset -= 1;
1263             for (int i = 0; i < runs.length; i += 2) {
1264                 int start = lineStart + runs[i];
1265                 int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
1266                 if (limit > lineEnd) {
1267                     limit = lineEnd;
1268                 }
1269                 if (offset >= start && offset < limit) {
1270                     levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
1271                     break;
1272                 }
1273             }
1274         }
1275 
1276         return levelBefore < levelAt;
1277     }
1278 
1279     /**
1280      * Computes in linear time the results of calling
1281      * #primaryIsTrailingPrevious for all offsets on a line.
1282      * @param line The line giving the offsets we compute the information for
1283      * @return The array of results, indexed from 0, where 0 corresponds to the line start offset
1284      * @hide
1285      */
1286     @VisibleForTesting
primaryIsTrailingPreviousAllLineOffsets(int line)1287     public boolean[] primaryIsTrailingPreviousAllLineOffsets(int line) {
1288         int lineStart = getLineStart(line);
1289         int lineEnd = getLineEnd(line);
1290         int[] runs = getLineDirections(line).mDirections;
1291 
1292         boolean[] trailing = new boolean[lineEnd - lineStart + 1];
1293 
1294         byte[] level = new byte[lineEnd - lineStart + 1];
1295         for (int i = 0; i < runs.length; i += 2) {
1296             int start = lineStart + runs[i];
1297             int limit = start + (runs[i + 1] & RUN_LENGTH_MASK);
1298             if (limit > lineEnd) {
1299                 limit = lineEnd;
1300             }
1301             if (limit == start) {
1302                 continue;
1303             }
1304             level[limit - lineStart - 1] =
1305                     (byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK);
1306         }
1307 
1308         for (int i = 0; i < runs.length; i += 2) {
1309             int start = lineStart + runs[i];
1310             byte currentLevel = (byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK);
1311             trailing[start - lineStart] = currentLevel > (start == lineStart
1312                     ? (getParagraphDirection(line) == 1 ? 0 : 1)
1313                     : level[start - lineStart - 1]);
1314         }
1315 
1316         return trailing;
1317     }
1318 
1319     /**
1320      * Get the primary horizontal position for the specified text offset.
1321      * This is the location where a new character would be inserted in
1322      * the paragraph's primary direction.
1323      */
getPrimaryHorizontal(int offset)1324     public float getPrimaryHorizontal(int offset) {
1325         return getPrimaryHorizontal(offset, false /* not clamped */);
1326     }
1327 
1328     /**
1329      * Get the primary horizontal position for the specified text offset, but
1330      * optionally clamp it so that it doesn't exceed the width of the layout.
1331      * @hide
1332      */
1333     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getPrimaryHorizontal(int offset, boolean clamped)1334     public float getPrimaryHorizontal(int offset, boolean clamped) {
1335         boolean trailing = primaryIsTrailingPrevious(offset);
1336         return getHorizontal(offset, trailing, clamped);
1337     }
1338 
1339     /**
1340      * Get the secondary horizontal position for the specified text offset.
1341      * This is the location where a new character would be inserted in
1342      * the direction other than the paragraph's primary direction.
1343      */
getSecondaryHorizontal(int offset)1344     public float getSecondaryHorizontal(int offset) {
1345         return getSecondaryHorizontal(offset, false /* not clamped */);
1346     }
1347 
1348     /**
1349      * Get the secondary horizontal position for the specified text offset, but
1350      * optionally clamp it so that it doesn't exceed the width of the layout.
1351      * @hide
1352      */
1353     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getSecondaryHorizontal(int offset, boolean clamped)1354     public float getSecondaryHorizontal(int offset, boolean clamped) {
1355         boolean trailing = primaryIsTrailingPrevious(offset);
1356         return getHorizontal(offset, !trailing, clamped);
1357     }
1358 
getHorizontal(int offset, boolean primary)1359     private float getHorizontal(int offset, boolean primary) {
1360         return primary ? getPrimaryHorizontal(offset) : getSecondaryHorizontal(offset);
1361     }
1362 
getHorizontal(int offset, boolean trailing, boolean clamped)1363     private float getHorizontal(int offset, boolean trailing, boolean clamped) {
1364         int line = getLineForOffset(offset);
1365 
1366         return getHorizontal(offset, trailing, line, clamped);
1367     }
1368 
getHorizontal(int offset, boolean trailing, int line, boolean clamped)1369     private float getHorizontal(int offset, boolean trailing, int line, boolean clamped) {
1370         int start = getLineStart(line);
1371         int end = getLineEnd(line);
1372         int dir = getParagraphDirection(line);
1373         boolean hasTab = getLineContainsTab(line);
1374         Directions directions = getLineDirections(line);
1375 
1376         TabStops tabStops = null;
1377         if (hasTab && mText instanceof Spanned) {
1378             // Just checking this line should be good enough, tabs should be
1379             // consistent across all lines in a paragraph.
1380             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
1381             if (tabs.length > 0) {
1382                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
1383             }
1384         }
1385 
1386         TextLine tl = TextLine.obtain();
1387         tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops,
1388                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
1389                 isFallbackLineSpacingEnabled());
1390         float wid = tl.measure(offset - start, trailing, null);
1391         TextLine.recycle(tl);
1392 
1393         if (clamped && wid > mWidth) {
1394             wid = mWidth;
1395         }
1396         int left = getParagraphLeft(line);
1397         int right = getParagraphRight(line);
1398 
1399         return getLineStartPos(line, left, right) + wid;
1400     }
1401 
1402     /**
1403      * Computes in linear time the results of calling #getHorizontal for all offsets on a line.
1404      *
1405      * @param line The line giving the offsets we compute information for
1406      * @param clamped Whether to clamp the results to the width of the layout
1407      * @param primary Whether the results should be the primary or the secondary horizontal
1408      * @return The array of results, indexed from 0, where 0 corresponds to the line start offset
1409      */
getLineHorizontals(int line, boolean clamped, boolean primary)1410     private float[] getLineHorizontals(int line, boolean clamped, boolean primary) {
1411         int start = getLineStart(line);
1412         int end = getLineEnd(line);
1413         int dir = getParagraphDirection(line);
1414         boolean hasTab = getLineContainsTab(line);
1415         Directions directions = getLineDirections(line);
1416 
1417         TabStops tabStops = null;
1418         if (hasTab && mText instanceof Spanned) {
1419             // Just checking this line should be good enough, tabs should be
1420             // consistent across all lines in a paragraph.
1421             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
1422             if (tabs.length > 0) {
1423                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
1424             }
1425         }
1426 
1427         TextLine tl = TextLine.obtain();
1428         tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops,
1429                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
1430                 isFallbackLineSpacingEnabled());
1431         boolean[] trailings = primaryIsTrailingPreviousAllLineOffsets(line);
1432         if (!primary) {
1433             for (int offset = 0; offset < trailings.length; ++offset) {
1434                 trailings[offset] = !trailings[offset];
1435             }
1436         }
1437         float[] wid = tl.measureAllOffsets(trailings, null);
1438         TextLine.recycle(tl);
1439 
1440         if (clamped) {
1441             for (int offset = 0; offset < wid.length; ++offset) {
1442                 if (wid[offset] > mWidth) {
1443                     wid[offset] = mWidth;
1444                 }
1445             }
1446         }
1447         int left = getParagraphLeft(line);
1448         int right = getParagraphRight(line);
1449 
1450         int lineStartPos = getLineStartPos(line, left, right);
1451         float[] horizontal = new float[end - start + 1];
1452         for (int offset = 0; offset < horizontal.length; ++offset) {
1453             horizontal[offset] = lineStartPos + wid[offset];
1454         }
1455         return horizontal;
1456     }
1457 
fillHorizontalBoundsForLine(int line, float[] horizontalBounds)1458     private void fillHorizontalBoundsForLine(int line, float[] horizontalBounds) {
1459         final int lineStart = getLineStart(line);
1460         final int lineEnd = getLineEnd(line);
1461         final int lineLength = lineEnd - lineStart;
1462 
1463         final int dir = getParagraphDirection(line);
1464         final Directions directions = getLineDirections(line);
1465 
1466         final boolean hasTab = getLineContainsTab(line);
1467         TabStops tabStops = null;
1468         if (hasTab && mText instanceof Spanned) {
1469             // Just checking this line should be good enough, tabs should be
1470             // consistent across all lines in a paragraph.
1471             TabStopSpan[] tabs =
1472                     getParagraphSpans((Spanned) mText, lineStart, lineEnd, TabStopSpan.class);
1473             if (tabs.length > 0) {
1474                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
1475             }
1476         }
1477 
1478         final TextLine tl = TextLine.obtain();
1479         tl.set(mPaint, mText, lineStart, lineEnd, dir, directions, hasTab, tabStops,
1480                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
1481                 isFallbackLineSpacingEnabled());
1482         if (horizontalBounds == null || horizontalBounds.length < 2 * lineLength) {
1483             horizontalBounds = new float[2 * lineLength];
1484         }
1485 
1486         tl.measureAllBounds(horizontalBounds, null);
1487         TextLine.recycle(tl);
1488     }
1489 
1490     /**
1491      * Return the characters' bounds in the given range. The {@code bounds} array will be filled
1492      * starting from {@code boundsStart} (inclusive). The coordinates are in local text layout.
1493      *
1494      * @param start the start index to compute the character bounds, inclusive.
1495      * @param end the end index to compute the character bounds, exclusive.
1496      * @param bounds the array to fill in the character bounds. The array is divided into segments
1497      *               of four where each index in that segment represents left, top, right and
1498      *               bottom of the character.
1499      * @param boundsStart the inclusive start index in the array to start filling in the values
1500      *                    from.
1501      *
1502      * @throws IndexOutOfBoundsException if the range defined by {@code start} and {@code end}
1503      * exceeds the range of the text, or {@code bounds} doesn't have enough space to store the
1504      * result.
1505      * @throws IllegalArgumentException if {@code bounds} is null.
1506      */
fillCharacterBounds(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull float[] bounds, @IntRange(from = 0) int boundsStart)1507     public void fillCharacterBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
1508             @NonNull float[] bounds, @IntRange(from = 0) int boundsStart) {
1509         if (start < 0 || end < start || end > mText.length()) {
1510             throw new IndexOutOfBoundsException("given range: " + start + ", " + end + " is "
1511                     + "out of the text range: 0, " + mText.length());
1512         }
1513 
1514         if (bounds == null) {
1515             throw  new IllegalArgumentException("bounds can't be null.");
1516         }
1517 
1518         final int neededLength = 4 * (end - start);
1519         if (neededLength > bounds.length - boundsStart) {
1520             throw new IndexOutOfBoundsException("bounds doesn't have enough space to store the "
1521                     + "result, needed: " + neededLength + " had: "
1522                     + (bounds.length - boundsStart));
1523         }
1524 
1525         if (start == end) {
1526             return;
1527         }
1528 
1529         final int startLine = getLineForOffset(start);
1530         final int endLine = getLineForOffset(end - 1);
1531         float[] horizontalBounds = null;
1532         for (int line = startLine; line <= endLine; ++line) {
1533             final int lineStart = getLineStart(line);
1534             final int lineEnd = getLineEnd(line);
1535             final int lineLength = lineEnd - lineStart;
1536             if (horizontalBounds == null || horizontalBounds.length < 2 * lineLength) {
1537                 horizontalBounds = new float[2 * lineLength];
1538             }
1539             fillHorizontalBoundsForLine(line, horizontalBounds);
1540 
1541             final int lineLeft = getParagraphLeft(line);
1542             final int lineRight = getParagraphRight(line);
1543             final int lineStartPos = getLineStartPos(line, lineLeft, lineRight);
1544 
1545             final int lineTop = getLineTop(line);
1546             final int lineBottom = getLineBottom(line);
1547 
1548             final int startIndex = Math.max(start, lineStart);
1549             final int endIndex = Math.min(end, lineEnd);
1550             for (int index = startIndex; index < endIndex; ++index) {
1551                 final int offset = index - lineStart;
1552                 final float left = horizontalBounds[offset * 2] + lineStartPos;
1553                 final float right = horizontalBounds[offset * 2 + 1] + lineStartPos;
1554 
1555                 final int boundsIndex = boundsStart + 4 * (index - start);
1556                 bounds[boundsIndex] = left;
1557                 bounds[boundsIndex + 1] = lineTop;
1558                 bounds[boundsIndex + 2] = right;
1559                 bounds[boundsIndex + 3] = lineBottom;
1560             }
1561         }
1562     }
1563 
1564     /**
1565      * Get the leftmost position that should be exposed for horizontal
1566      * scrolling on the specified line.
1567      */
getLineLeft(int line)1568     public float getLineLeft(int line) {
1569         final int dir = getParagraphDirection(line);
1570         Alignment align = getParagraphAlignment(line);
1571         // Before Q, StaticLayout.Builder.setAlignment didn't check whether the input alignment
1572         // is null. And when it is null, the old behavior is the same as ALIGN_CENTER.
1573         // To keep consistency, we convert a null alignment to ALIGN_CENTER.
1574         if (align == null) {
1575             align = Alignment.ALIGN_CENTER;
1576         }
1577 
1578         // First convert combinations of alignment and direction settings to
1579         // three basic cases: ALIGN_LEFT, ALIGN_RIGHT and ALIGN_CENTER.
1580         // For unexpected cases, it will fallback to ALIGN_LEFT.
1581         final Alignment resultAlign;
1582         switch(align) {
1583             case ALIGN_NORMAL:
1584                 resultAlign =
1585                         dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_RIGHT : Alignment.ALIGN_LEFT;
1586                 break;
1587             case ALIGN_OPPOSITE:
1588                 resultAlign =
1589                         dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_LEFT : Alignment.ALIGN_RIGHT;
1590                 break;
1591             case ALIGN_CENTER:
1592                 resultAlign = Alignment.ALIGN_CENTER;
1593                 break;
1594             case ALIGN_RIGHT:
1595                 resultAlign = Alignment.ALIGN_RIGHT;
1596                 break;
1597             default: /* align == Alignment.ALIGN_LEFT */
1598                 resultAlign = Alignment.ALIGN_LEFT;
1599         }
1600 
1601         // Here we must use getLineMax() to do the computation, because it maybe overridden by
1602         // derived class. And also note that line max equals the width of the text in that line
1603         // plus the leading margin.
1604         switch (resultAlign) {
1605             case ALIGN_CENTER:
1606                 final int left = getParagraphLeft(line);
1607                 final float max = getLineMax(line);
1608                 // This computation only works when mWidth equals leadingMargin plus
1609                 // the width of text in this line. If this condition doesn't meet anymore,
1610                 // please change here too.
1611                 return (float) Math.floor(left + (mWidth - max) / 2);
1612             case ALIGN_RIGHT:
1613                 return mWidth - getLineMax(line);
1614             default: /* resultAlign == Alignment.ALIGN_LEFT */
1615                 return 0;
1616         }
1617     }
1618 
1619     /**
1620      * Get the rightmost position that should be exposed for horizontal
1621      * scrolling on the specified line.
1622      */
getLineRight(int line)1623     public float getLineRight(int line) {
1624         final int dir = getParagraphDirection(line);
1625         Alignment align = getParagraphAlignment(line);
1626         // Before Q, StaticLayout.Builder.setAlignment didn't check whether the input alignment
1627         // is null. And when it is null, the old behavior is the same as ALIGN_CENTER.
1628         // To keep consistency, we convert a null alignment to ALIGN_CENTER.
1629         if (align == null) {
1630             align = Alignment.ALIGN_CENTER;
1631         }
1632 
1633         final Alignment resultAlign;
1634         switch(align) {
1635             case ALIGN_NORMAL:
1636                 resultAlign =
1637                         dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_RIGHT : Alignment.ALIGN_LEFT;
1638                 break;
1639             case ALIGN_OPPOSITE:
1640                 resultAlign =
1641                         dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_LEFT : Alignment.ALIGN_RIGHT;
1642                 break;
1643             case ALIGN_CENTER:
1644                 resultAlign = Alignment.ALIGN_CENTER;
1645                 break;
1646             case ALIGN_RIGHT:
1647                 resultAlign = Alignment.ALIGN_RIGHT;
1648                 break;
1649             default: /* align == Alignment.ALIGN_LEFT */
1650                 resultAlign = Alignment.ALIGN_LEFT;
1651         }
1652 
1653         switch (resultAlign) {
1654             case ALIGN_CENTER:
1655                 final int right = getParagraphRight(line);
1656                 final float max = getLineMax(line);
1657                 // This computation only works when mWidth equals leadingMargin plus width of the
1658                 // text in this line. If this condition doesn't meet anymore, please change here.
1659                 return (float) Math.ceil(right - (mWidth - max) / 2);
1660             case ALIGN_RIGHT:
1661                 return mWidth;
1662             default: /* resultAlign == Alignment.ALIGN_LEFT */
1663                 return getLineMax(line);
1664         }
1665     }
1666 
1667     /**
1668      * Gets the unsigned horizontal extent of the specified line, including
1669      * leading margin indent, but excluding trailing whitespace.
1670      */
getLineMax(int line)1671     public float getLineMax(int line) {
1672         float margin = getParagraphLeadingMargin(line);
1673         float signedExtent = getLineExtent(line, false);
1674         return margin + (signedExtent >= 0 ? signedExtent : -signedExtent);
1675     }
1676 
1677     /**
1678      * Gets the unsigned horizontal extent of the specified line, including
1679      * leading margin indent and trailing whitespace.
1680      */
getLineWidth(int line)1681     public float getLineWidth(int line) {
1682         float margin = getParagraphLeadingMargin(line);
1683         float signedExtent = getLineExtent(line, true);
1684         return margin + (signedExtent >= 0 ? signedExtent : -signedExtent);
1685     }
1686 
1687     /**
1688      * Like {@link #getLineExtent(int,TabStops,boolean)} but determines the
1689      * tab stops instead of using the ones passed in.
1690      * @param line the index of the line
1691      * @param full whether to include trailing whitespace
1692      * @return the extent of the line
1693      */
getLineExtent(int line, boolean full)1694     private float getLineExtent(int line, boolean full) {
1695         final int start = getLineStart(line);
1696         final int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
1697 
1698         final boolean hasTabs = getLineContainsTab(line);
1699         TabStops tabStops = null;
1700         if (hasTabs && mText instanceof Spanned) {
1701             // Just checking this line should be good enough, tabs should be
1702             // consistent across all lines in a paragraph.
1703             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
1704             if (tabs.length > 0) {
1705                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
1706             }
1707         }
1708         final Directions directions = getLineDirections(line);
1709         // Returned directions can actually be null
1710         if (directions == null) {
1711             return 0f;
1712         }
1713         final int dir = getParagraphDirection(line);
1714 
1715         final TextLine tl = TextLine.obtain();
1716         final TextPaint paint = mWorkPaint;
1717         paint.set(mPaint);
1718         paint.setStartHyphenEdit(getStartHyphenEdit(line));
1719         paint.setEndHyphenEdit(getEndHyphenEdit(line));
1720         tl.set(paint, mText, start, end, dir, directions, hasTabs, tabStops,
1721                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
1722                 isFallbackLineSpacingEnabled());
1723         if (isJustificationRequired(line)) {
1724             tl.justify(getJustifyWidth(line));
1725         }
1726         final float width = tl.metrics(null);
1727         TextLine.recycle(tl);
1728         return width;
1729     }
1730 
1731     /**
1732      * Returns the signed horizontal extent of the specified line, excluding
1733      * leading margin.  If full is false, excludes trailing whitespace.
1734      * @param line the index of the line
1735      * @param tabStops the tab stops, can be null if we know they're not used.
1736      * @param full whether to include trailing whitespace
1737      * @return the extent of the text on this line
1738      */
getLineExtent(int line, TabStops tabStops, boolean full)1739     private float getLineExtent(int line, TabStops tabStops, boolean full) {
1740         final int start = getLineStart(line);
1741         final int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
1742         final boolean hasTabs = getLineContainsTab(line);
1743         final Directions directions = getLineDirections(line);
1744         final int dir = getParagraphDirection(line);
1745 
1746         final TextLine tl = TextLine.obtain();
1747         final TextPaint paint = mWorkPaint;
1748         paint.set(mPaint);
1749         paint.setStartHyphenEdit(getStartHyphenEdit(line));
1750         paint.setEndHyphenEdit(getEndHyphenEdit(line));
1751         tl.set(paint, mText, start, end, dir, directions, hasTabs, tabStops,
1752                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
1753                 isFallbackLineSpacingEnabled());
1754         if (isJustificationRequired(line)) {
1755             tl.justify(getJustifyWidth(line));
1756         }
1757         final float width = tl.metrics(null);
1758         TextLine.recycle(tl);
1759         return width;
1760     }
1761 
1762     /**
1763      * Get the line number corresponding to the specified vertical position.
1764      * If you ask for a position above 0, you get 0; if you ask for a position
1765      * below the bottom of the text, you get the last line.
1766      */
1767     // FIXME: It may be faster to do a linear search for layouts without many lines.
getLineForVertical(int vertical)1768     public int getLineForVertical(int vertical) {
1769         int high = getLineCount(), low = -1, guess;
1770 
1771         while (high - low > 1) {
1772             guess = (high + low) / 2;
1773 
1774             if (getLineTop(guess) > vertical)
1775                 high = guess;
1776             else
1777                 low = guess;
1778         }
1779 
1780         if (low < 0)
1781             return 0;
1782         else
1783             return low;
1784     }
1785 
1786     /**
1787      * Get the line number on which the specified text offset appears.
1788      * If you ask for a position before 0, you get 0; if you ask for a position
1789      * beyond the end of the text, you get the last line.
1790      */
getLineForOffset(int offset)1791     public int getLineForOffset(int offset) {
1792         int high = getLineCount(), low = -1, guess;
1793 
1794         while (high - low > 1) {
1795             guess = (high + low) / 2;
1796 
1797             if (getLineStart(guess) > offset)
1798                 high = guess;
1799             else
1800                 low = guess;
1801         }
1802 
1803         if (low < 0) {
1804             return 0;
1805         } else {
1806             return low;
1807         }
1808     }
1809 
1810     /**
1811      * Get the character offset on the specified line whose position is
1812      * closest to the specified horizontal position.
1813      */
getOffsetForHorizontal(int line, float horiz)1814     public int getOffsetForHorizontal(int line, float horiz) {
1815         return getOffsetForHorizontal(line, horiz, true);
1816     }
1817 
1818     /**
1819      * Get the character offset on the specified line whose position is
1820      * closest to the specified horizontal position.
1821      *
1822      * @param line the line used to find the closest offset
1823      * @param horiz the horizontal position used to find the closest offset
1824      * @param primary whether to use the primary position or secondary position to find the offset
1825      *
1826      * @hide
1827      */
getOffsetForHorizontal(int line, float horiz, boolean primary)1828     public int getOffsetForHorizontal(int line, float horiz, boolean primary) {
1829         // TODO: use Paint.getOffsetForAdvance to avoid binary search
1830         final int lineEndOffset = getLineEnd(line);
1831         final int lineStartOffset = getLineStart(line);
1832 
1833         Directions dirs = getLineDirections(line);
1834 
1835         TextLine tl = TextLine.obtain();
1836         // XXX: we don't care about tabs as we just use TextLine#getOffsetToLeftRightOf here.
1837         tl.set(mPaint, mText, lineStartOffset, lineEndOffset, getParagraphDirection(line), dirs,
1838                 false, null,
1839                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
1840                 isFallbackLineSpacingEnabled());
1841         final HorizontalMeasurementProvider horizontal =
1842                 new HorizontalMeasurementProvider(line, primary);
1843 
1844         final int max;
1845         if (line == getLineCount() - 1) {
1846             max = lineEndOffset;
1847         } else {
1848             max = tl.getOffsetToLeftRightOf(lineEndOffset - lineStartOffset,
1849                     !isRtlCharAt(lineEndOffset - 1)) + lineStartOffset;
1850         }
1851         int best = lineStartOffset;
1852         float bestdist = Math.abs(horizontal.get(lineStartOffset) - horiz);
1853 
1854         for (int i = 0; i < dirs.mDirections.length; i += 2) {
1855             int here = lineStartOffset + dirs.mDirections[i];
1856             int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK);
1857             boolean isRtl = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0;
1858             int swap = isRtl ? -1 : 1;
1859 
1860             if (there > max)
1861                 there = max;
1862             int high = there - 1 + 1, low = here + 1 - 1, guess;
1863 
1864             while (high - low > 1) {
1865                 guess = (high + low) / 2;
1866                 int adguess = getOffsetAtStartOf(guess);
1867 
1868                 if (horizontal.get(adguess) * swap >= horiz * swap) {
1869                     high = guess;
1870                 } else {
1871                     low = guess;
1872                 }
1873             }
1874 
1875             if (low < here + 1)
1876                 low = here + 1;
1877 
1878             if (low < there) {
1879                 int aft = tl.getOffsetToLeftRightOf(low - lineStartOffset, isRtl) + lineStartOffset;
1880                 low = tl.getOffsetToLeftRightOf(aft - lineStartOffset, !isRtl) + lineStartOffset;
1881                 if (low >= here && low < there) {
1882                     float dist = Math.abs(horizontal.get(low) - horiz);
1883                     if (aft < there) {
1884                         float other = Math.abs(horizontal.get(aft) - horiz);
1885 
1886                         if (other < dist) {
1887                             dist = other;
1888                             low = aft;
1889                         }
1890                     }
1891 
1892                     if (dist < bestdist) {
1893                         bestdist = dist;
1894                         best = low;
1895                     }
1896                 }
1897             }
1898 
1899             float dist = Math.abs(horizontal.get(here) - horiz);
1900 
1901             if (dist < bestdist) {
1902                 bestdist = dist;
1903                 best = here;
1904             }
1905         }
1906 
1907         float dist = Math.abs(horizontal.get(max) - horiz);
1908 
1909         if (dist <= bestdist) {
1910             best = max;
1911         }
1912 
1913         TextLine.recycle(tl);
1914         return best;
1915     }
1916 
1917     /**
1918      * Responds to #getHorizontal queries, by selecting the better strategy between:
1919      * - calling #getHorizontal explicitly for each query
1920      * - precomputing all #getHorizontal measurements, and responding to any query in constant time
1921      * The first strategy is used for LTR-only text, while the second is used for all other cases.
1922      * The class is currently only used in #getOffsetForHorizontal, so reuse with care in other
1923      * contexts.
1924      */
1925     private class HorizontalMeasurementProvider {
1926         private final int mLine;
1927         private final boolean mPrimary;
1928 
1929         private float[] mHorizontals;
1930         private int mLineStartOffset;
1931 
HorizontalMeasurementProvider(final int line, final boolean primary)1932         HorizontalMeasurementProvider(final int line, final boolean primary) {
1933             mLine = line;
1934             mPrimary = primary;
1935             init();
1936         }
1937 
init()1938         private void init() {
1939             final Directions dirs = getLineDirections(mLine);
1940             if (dirs == DIRS_ALL_LEFT_TO_RIGHT) {
1941                 return;
1942             }
1943 
1944             mHorizontals = getLineHorizontals(mLine, false, mPrimary);
1945             mLineStartOffset = getLineStart(mLine);
1946         }
1947 
get(final int offset)1948         float get(final int offset) {
1949             final int index = offset - mLineStartOffset;
1950             if (mHorizontals == null || index < 0 || index >= mHorizontals.length) {
1951                 return getHorizontal(offset, mPrimary);
1952             } else {
1953                 return mHorizontals[index];
1954             }
1955         }
1956     }
1957 
1958     /**
1959      * Finds the range of text which is inside the specified rectangle area. The start of the range
1960      * is the start of the first text segment inside the area, and the end of the range is the end
1961      * of the last text segment inside the area.
1962      *
1963      * <p>A text segment is considered to be inside the area according to the provided {@link
1964      * TextInclusionStrategy}. If a text segment spans multiple lines or multiple directional runs
1965      * (e.g. a hyphenated word), the text segment is divided into pieces at the line and run breaks,
1966      * then the text segment is considered to be inside the area if any of its pieces are inside the
1967      * area.
1968      *
1969      * <p>The returned range may also include text segments which are not inside the specified area,
1970      * if those text segments are in between text segments which are inside the area. For example,
1971      * the returned range may be "segment1 segment2 segment3" if "segment1" and "segment3" are
1972      * inside the area and "segment2" is not.
1973      *
1974      * @param area area for which the text range will be found
1975      * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a
1976      *     text segment
1977      * @param inclusionStrategy strategy for determining whether a text segment is inside the
1978      *     specified area
1979      * @return int array of size 2 containing the start (inclusive) and end (exclusive) character
1980      *     offsets of the text range, or null if there are no text segments inside the area
1981      */
1982     @Nullable
getRangeForRect(@onNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull TextInclusionStrategy inclusionStrategy)1983     public int[] getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder,
1984             @NonNull TextInclusionStrategy inclusionStrategy) {
1985         // Find the first line whose bottom (without line spacing) is below the top of the area.
1986         int startLine = getLineForVertical((int) area.top);
1987         if (area.top > getLineBottom(startLine, /* includeLineSpacing= */ false)) {
1988             startLine++;
1989             if (startLine >= getLineCount()) {
1990                 // The entire area is below the last line, so it does not contain any text.
1991                 return null;
1992             }
1993         }
1994 
1995         // Find the last line whose top is above the bottom of the area.
1996         int endLine = getLineForVertical((int) area.bottom);
1997         if (endLine == 0 && area.bottom < getLineTop(0)) {
1998             // The entire area is above the first line, so it does not contain any text.
1999             return null;
2000         }
2001         if (endLine < startLine) {
2002             // The entire area is between two lines, so it does not contain any text.
2003             return null;
2004         }
2005 
2006         int start = getStartOrEndOffsetForAreaWithinLine(
2007                 startLine, area, segmentFinder, inclusionStrategy, /* getStart= */ true);
2008         // If the area does not contain any text on this line, keep trying subsequent lines until
2009         // the end line is reached.
2010         while (start == -1 && startLine < endLine) {
2011             startLine++;
2012             start = getStartOrEndOffsetForAreaWithinLine(
2013                     startLine, area, segmentFinder, inclusionStrategy, /* getStart= */ true);
2014         }
2015         if (start == -1) {
2016             // All lines were checked, the area does not contain any text.
2017             return null;
2018         }
2019 
2020         int end = getStartOrEndOffsetForAreaWithinLine(
2021                 endLine, area, segmentFinder, inclusionStrategy, /* getStart= */ false);
2022         // If the area does not contain any text on this line, keep trying previous lines until
2023         // the start line is reached.
2024         while (end == -1 && startLine < endLine) {
2025             endLine--;
2026             end = getStartOrEndOffsetForAreaWithinLine(
2027                     endLine, area, segmentFinder, inclusionStrategy, /* getStart= */ false);
2028         }
2029         if (end == -1) {
2030             // All lines were checked, the area does not contain any text.
2031             return null;
2032         }
2033 
2034         // If a text segment spans multiple lines or multiple directional runs (e.g. a hyphenated
2035         // word), then getStartOrEndOffsetForAreaWithinLine() can return an offset in the middle of
2036         // a text segment. Adjust the range to include the rest of any partial text segments. If
2037         // start is already the start boundary of a text segment, then this is a no-op.
2038         start = segmentFinder.previousStartBoundary(start + 1);
2039         end = segmentFinder.nextEndBoundary(end - 1);
2040 
2041         return new int[] {start, end};
2042     }
2043 
2044     /**
2045      * Finds the start character offset of the first text segment within a line inside the specified
2046      * rectangle area, or the end character offset of the last text segment inside the area.
2047      *
2048      * @param line index of the line to search
2049      * @param area area inside which text segments will be found
2050      * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a
2051      *     text segment
2052      * @param inclusionStrategy strategy for determining whether a text segment is inside the
2053      *     specified area
2054      * @param getStart true to find the start of the first text segment inside the area, false to
2055      *     find the end of the last text segment
2056      * @return the start character offset of the first text segment inside the area, or the end
2057      *     character offset of the last text segment inside the area.
2058      */
getStartOrEndOffsetForAreaWithinLine( @ntRangefrom = 0) int line, @NonNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull TextInclusionStrategy inclusionStrategy, boolean getStart)2059     private int getStartOrEndOffsetForAreaWithinLine(
2060             @IntRange(from = 0) int line,
2061             @NonNull RectF area,
2062             @NonNull SegmentFinder segmentFinder,
2063             @NonNull TextInclusionStrategy inclusionStrategy,
2064             boolean getStart) {
2065         int lineTop = getLineTop(line);
2066         int lineBottom = getLineBottom(line, /* includeLineSpacing= */ false);
2067 
2068         int lineStartOffset = getLineStart(line);
2069         int lineEndOffset = getLineEnd(line);
2070         if (lineStartOffset == lineEndOffset) {
2071             return -1;
2072         }
2073 
2074         float[] horizontalBounds = new float[2 * (lineEndOffset - lineStartOffset)];
2075         fillHorizontalBoundsForLine(line, horizontalBounds);
2076 
2077         int lineStartPos = getLineStartPos(line, getParagraphLeft(line), getParagraphRight(line));
2078 
2079         // Loop through the runs forwards or backwards depending on getStart value.
2080         Layout.Directions directions = getLineDirections(line);
2081         int runIndex = getStart ? 0 : directions.getRunCount() - 1;
2082         while ((getStart && runIndex < directions.getRunCount()) || (!getStart && runIndex >= 0)) {
2083             // runStartOffset and runEndOffset are offset indices within the line.
2084             int runStartOffset = directions.getRunStart(runIndex);
2085             int runEndOffset = Math.min(
2086                     runStartOffset + directions.getRunLength(runIndex),
2087                     lineEndOffset - lineStartOffset);
2088             boolean isRtl = directions.isRunRtl(runIndex);
2089             float runLeft = lineStartPos
2090                     + (isRtl
2091                             ? horizontalBounds[2 * (runEndOffset - 1)]
2092                             : horizontalBounds[2 * runStartOffset]);
2093             float runRight = lineStartPos
2094                     + (isRtl
2095                             ? horizontalBounds[2 * runStartOffset + 1]
2096                             : horizontalBounds[2 * (runEndOffset - 1) + 1]);
2097 
2098             int result =
2099                     getStart
2100                             ? getStartOffsetForAreaWithinRun(
2101                                     area, lineTop, lineBottom,
2102                                     lineStartOffset, lineStartPos, horizontalBounds,
2103                                     runStartOffset, runEndOffset, runLeft, runRight, isRtl,
2104                                     segmentFinder, inclusionStrategy)
2105                             : getEndOffsetForAreaWithinRun(
2106                                     area, lineTop, lineBottom,
2107                                     lineStartOffset, lineStartPos, horizontalBounds,
2108                                     runStartOffset, runEndOffset, runLeft, runRight, isRtl,
2109                                     segmentFinder, inclusionStrategy);
2110             if (result >= 0) {
2111                 return result;
2112             }
2113 
2114             runIndex += getStart ? 1 : -1;
2115         }
2116         return -1;
2117     }
2118 
2119     /**
2120      * Finds the start character offset of the first text segment within a directional run inside
2121      * the specified rectangle area.
2122      *
2123      * @param area area inside which text segments will be found
2124      * @param lineTop top of the line containing this run
2125      * @param lineBottom bottom (not including line spacing) of the line containing this run
2126      * @param lineStartOffset start character offset of the line containing this run
2127      * @param lineStartPos start position of the line containing this run
2128      * @param horizontalBounds array containing the signed horizontal bounds of the characters in
2129      *     the line. The left and right bounds of the character at offset i are stored at index (2 *
2130      *     i) and index (2 * i + 1). Bounds are relative to {@code lineStartPos}.
2131      * @param runStartOffset start offset of the run relative to {@code lineStartOffset}
2132      * @param runEndOffset end offset of the run relative to {@code lineStartOffset}
2133      * @param runLeft left bound of the run
2134      * @param runRight right bound of the run
2135      * @param isRtl whether the run is right-to-left
2136      * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a
2137      *     text segment
2138      * @param inclusionStrategy strategy for determining whether a text segment is inside the
2139      *     specified area
2140      * @return the start character offset of the first text segment inside the area
2141      */
getStartOffsetForAreaWithinRun( @onNull RectF area, int lineTop, int lineBottom, @IntRange(from = 0) int lineStartOffset, @IntRange(from = 0) int lineStartPos, @NonNull float[] horizontalBounds, @IntRange(from = 0) int runStartOffset, @IntRange(from = 0) int runEndOffset, float runLeft, float runRight, boolean isRtl, @NonNull SegmentFinder segmentFinder, @NonNull TextInclusionStrategy inclusionStrategy)2142     private static int getStartOffsetForAreaWithinRun(
2143             @NonNull RectF area,
2144             int lineTop, int lineBottom,
2145             @IntRange(from = 0) int lineStartOffset,
2146             @IntRange(from = 0) int lineStartPos,
2147             @NonNull float[] horizontalBounds,
2148             @IntRange(from = 0) int runStartOffset, @IntRange(from = 0) int runEndOffset,
2149             float runLeft, float runRight,
2150             boolean isRtl,
2151             @NonNull SegmentFinder segmentFinder,
2152             @NonNull TextInclusionStrategy inclusionStrategy) {
2153         if (runRight < area.left || runLeft > area.right) {
2154             // The run does not overlap the area.
2155             return -1;
2156         }
2157 
2158         // Find the first character in the run whose bounds overlap with the area.
2159         // firstCharOffset is an offset index within the line.
2160         int firstCharOffset;
2161         if ((!isRtl && area.left <= runLeft) || (isRtl && area.right >= runRight)) {
2162             firstCharOffset = runStartOffset;
2163         } else {
2164             int low = runStartOffset;
2165             int high = runEndOffset;
2166             int guess;
2167             while (high - low > 1) {
2168                 guess = (high + low) / 2;
2169                 // Left edge of the character at guess
2170                 float pos = lineStartPos + horizontalBounds[2 * guess];
2171                 if ((!isRtl && pos > area.left) || (isRtl && pos < area.right)) {
2172                     high = guess;
2173                 } else {
2174                     low = guess;
2175                 }
2176             }
2177             // The area edge is between the left edge of the character at low and the left edge of
2178             // the character at high. For LTR text, this is within the character at low. For RTL
2179             // text, this is within the character at high.
2180             firstCharOffset = isRtl ? high : low;
2181         }
2182 
2183         // Find the first text segment containing this character (or, if no text segment contains
2184         // this character, the first text segment after this character). All previous text segments
2185         // in this run are to the left (for LTR) of the area.
2186         int segmentEndOffset =
2187                 segmentFinder.nextEndBoundary(lineStartOffset + firstCharOffset);
2188         if (segmentEndOffset == SegmentFinder.DONE) {
2189             // There are no text segments containing or after firstCharOffset, so no text segments
2190             // in this run overlap the area.
2191             return -1;
2192         }
2193         int segmentStartOffset = segmentFinder.previousStartBoundary(segmentEndOffset);
2194         if (segmentStartOffset >= lineStartOffset + runEndOffset) {
2195             // The text segment is after the end of this run, so no text segments in this run
2196             // overlap the area.
2197             return -1;
2198         }
2199         // If the segment extends outside of this run, only consider the piece of the segment within
2200         // this run.
2201         segmentStartOffset = Math.max(segmentStartOffset, lineStartOffset + runStartOffset);
2202         segmentEndOffset = Math.min(segmentEndOffset, lineStartOffset + runEndOffset);
2203 
2204         RectF segmentBounds = new RectF(0, lineTop, 0, lineBottom);
2205         while (true) {
2206             // Start (left for LTR, right for RTL) edge of the character at segmentStartOffset.
2207             float segmentStart = lineStartPos + horizontalBounds[
2208                     2 * (segmentStartOffset - lineStartOffset) + (isRtl ? 1 : 0)];
2209             if ((!isRtl && segmentStart > area.right) || (isRtl && segmentStart < area.left)) {
2210                 // The entire area is to the left (for LTR) of the text segment. So the area does
2211                 // not contain any text segments within this run.
2212                 return -1;
2213             }
2214             // End (right for LTR, left for RTL) edge of the character at (segmentStartOffset - 1).
2215             float segmentEnd = lineStartPos + horizontalBounds[
2216                     2 * (segmentEndOffset - lineStartOffset - 1) + (isRtl ? 0 : 1)];
2217             segmentBounds.left = isRtl ? segmentEnd : segmentStart;
2218             segmentBounds.right = isRtl ? segmentStart : segmentEnd;
2219             if (inclusionStrategy.isSegmentInside(segmentBounds, area)) {
2220                 return segmentStartOffset;
2221             }
2222             // Try the next text segment.
2223             segmentStartOffset = segmentFinder.nextStartBoundary(segmentStartOffset);
2224             if (segmentStartOffset == SegmentFinder.DONE
2225                     || segmentStartOffset >= lineStartOffset + runEndOffset) {
2226                 // No more text segments within this run.
2227                 return -1;
2228             }
2229             segmentEndOffset = segmentFinder.nextEndBoundary(segmentStartOffset);
2230             // If the segment extends past the end of this run, only consider the piece of the
2231             // segment within this run.
2232             segmentEndOffset = Math.min(segmentEndOffset, lineStartOffset + runEndOffset);
2233         }
2234     }
2235 
2236     /**
2237      * Finds the end character offset of the last text segment within a directional run inside the
2238      * specified rectangle area.
2239      *
2240      * @param area area inside which text segments will be found
2241      * @param lineTop top of the line containing this run
2242      * @param lineBottom bottom (not including line spacing) of the line containing this run
2243      * @param lineStartOffset start character offset of the line containing this run
2244      * @param lineStartPos start position of the line containing this run
2245      * @param horizontalBounds array containing the signed horizontal bounds of the characters in
2246      *     the line. The left and right bounds of the character at offset i are stored at index (2 *
2247      *     i) and index (2 * i + 1). Bounds are relative to {@code lineStartPos}.
2248      * @param runStartOffset start offset of the run relative to {@code lineStartOffset}
2249      * @param runEndOffset end offset of the run relative to {@code lineStartOffset}
2250      * @param runLeft left bound of the run
2251      * @param runRight right bound of the run
2252      * @param isRtl whether the run is right-to-left
2253      * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a
2254      *     text segment
2255      * @param inclusionStrategy strategy for determining whether a text segment is inside the
2256      *     specified area
2257      * @return the end character offset of the last text segment inside the area
2258      */
getEndOffsetForAreaWithinRun( @onNull RectF area, int lineTop, int lineBottom, @IntRange(from = 0) int lineStartOffset, @IntRange(from = 0) int lineStartPos, @NonNull float[] horizontalBounds, @IntRange(from = 0) int runStartOffset, @IntRange(from = 0) int runEndOffset, float runLeft, float runRight, boolean isRtl, @NonNull SegmentFinder segmentFinder, @NonNull TextInclusionStrategy inclusionStrategy)2259     private static int getEndOffsetForAreaWithinRun(
2260             @NonNull RectF area,
2261             int lineTop, int lineBottom,
2262             @IntRange(from = 0) int lineStartOffset,
2263             @IntRange(from = 0) int lineStartPos,
2264             @NonNull float[] horizontalBounds,
2265             @IntRange(from = 0) int runStartOffset, @IntRange(from = 0) int runEndOffset,
2266             float runLeft, float runRight,
2267             boolean isRtl,
2268             @NonNull SegmentFinder segmentFinder,
2269             @NonNull TextInclusionStrategy inclusionStrategy) {
2270         if (runRight < area.left || runLeft > area.right) {
2271             // The run does not overlap the area.
2272             return -1;
2273         }
2274 
2275         // Find the last character in the run whose bounds overlap with the area.
2276         // firstCharOffset is an offset index within the line.
2277         int lastCharOffset;
2278         if ((!isRtl && area.right >= runRight) || (isRtl && area.left <= runLeft)) {
2279             lastCharOffset = runEndOffset - 1;
2280         } else {
2281             int low = runStartOffset;
2282             int high = runEndOffset;
2283             int guess;
2284             while (high - low > 1) {
2285                 guess = (high + low) / 2;
2286                 // Left edge of the character at guess
2287                 float pos = lineStartPos + horizontalBounds[2 * guess];
2288                 if ((!isRtl && pos > area.right) || (isRtl && pos < area.left)) {
2289                     high = guess;
2290                 } else {
2291                     low = guess;
2292                 }
2293             }
2294             // The area edge is between the left edge of the character at low and the left edge of
2295             // the character at high. For LTR text, this is within the character at low. For RTL
2296             // text, this is within the character at high.
2297             lastCharOffset = isRtl ? high : low;
2298         }
2299 
2300         // Find the last text segment containing this character (or, if no text segment contains
2301         // this character, the first text segment before this character). All following text
2302         // segments in this run are to the right (for LTR) of the area.
2303         // + 1 to allow segmentStartOffset = lineStartOffset + lastCharOffset
2304         int segmentStartOffset =
2305                 segmentFinder.previousStartBoundary(lineStartOffset + lastCharOffset + 1);
2306         if (segmentStartOffset == SegmentFinder.DONE) {
2307             // There are no text segments containing or before lastCharOffset, so no text segments
2308             // in this run overlap the area.
2309             return -1;
2310         }
2311         int segmentEndOffset = segmentFinder.nextEndBoundary(segmentStartOffset);
2312         if (segmentEndOffset <= lineStartOffset + runStartOffset) {
2313             // The text segment is before the start of this run, so no text segments in this run
2314             // overlap the area.
2315             return -1;
2316         }
2317         // If the segment extends outside of this run, only consider the piece of the segment within
2318         // this run.
2319         segmentStartOffset = Math.max(segmentStartOffset, lineStartOffset + runStartOffset);
2320         segmentEndOffset = Math.min(segmentEndOffset, lineStartOffset + runEndOffset);
2321 
2322         RectF segmentBounds = new RectF(0, lineTop, 0, lineBottom);
2323         while (true) {
2324             // End (right for LTR, left for RTL) edge of the character at (segmentStartOffset - 1).
2325             float segmentEnd = lineStartPos + horizontalBounds[
2326                     2 * (segmentEndOffset - lineStartOffset - 1) + (isRtl ? 0 : 1)];
2327             if ((!isRtl && segmentEnd < area.left) || (isRtl && segmentEnd > area.right)) {
2328                 // The entire area is to the right (for LTR) of the text segment. So the
2329                 // area does not contain any text segments within this run.
2330                 return -1;
2331             }
2332             // Start (left for LTR, right for RTL) edge of the character at segmentStartOffset.
2333             float segmentStart = lineStartPos + horizontalBounds[
2334                     2 * (segmentStartOffset - lineStartOffset) + (isRtl ? 1 : 0)];
2335             segmentBounds.left = isRtl ? segmentEnd : segmentStart;
2336             segmentBounds.right = isRtl ? segmentStart : segmentEnd;
2337             if (inclusionStrategy.isSegmentInside(segmentBounds, area)) {
2338                 return segmentEndOffset;
2339             }
2340             // Try the previous text segment.
2341             segmentEndOffset = segmentFinder.previousEndBoundary(segmentEndOffset);
2342             if (segmentEndOffset == SegmentFinder.DONE
2343                     || segmentEndOffset <= lineStartOffset + runStartOffset) {
2344                 // No more text segments within this run.
2345                 return -1;
2346             }
2347             segmentStartOffset = segmentFinder.previousStartBoundary(segmentEndOffset);
2348             // If the segment extends past the start of this run, only consider the piece of the
2349             // segment within this run.
2350             segmentStartOffset = Math.max(segmentStartOffset, lineStartOffset + runStartOffset);
2351         }
2352     }
2353 
2354     /**
2355      * Return the text offset after the last character on the specified line.
2356      */
getLineEnd(int line)2357     public final int getLineEnd(int line) {
2358         return getLineStart(line + 1);
2359     }
2360 
2361     /**
2362      * Return the text offset after the last visible character (so whitespace
2363      * is not counted) on the specified line.
2364      */
getLineVisibleEnd(int line)2365     public int getLineVisibleEnd(int line) {
2366         return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1));
2367     }
2368 
getLineVisibleEnd(int line, int start, int end)2369     private int getLineVisibleEnd(int line, int start, int end) {
2370         CharSequence text = mText;
2371         char ch;
2372         if (line == getLineCount() - 1) {
2373             return end;
2374         }
2375 
2376         for (; end > start; end--) {
2377             ch = text.charAt(end - 1);
2378 
2379             if (ch == '\n') {
2380                 return end - 1;
2381             }
2382 
2383             if (!TextLine.isLineEndSpace(ch)) {
2384                 break;
2385             }
2386 
2387         }
2388 
2389         return end;
2390     }
2391 
2392     /**
2393      * Return the vertical position of the bottom of the specified line.
2394      */
getLineBottom(int line)2395     public final int getLineBottom(int line) {
2396         return getLineBottom(line, /* includeLineSpacing= */ true);
2397     }
2398 
2399     /**
2400      * Return the vertical position of the bottom of the specified line.
2401      *
2402      * @param line index of the line
2403      * @param includeLineSpacing whether to include the line spacing
2404      */
getLineBottom(int line, boolean includeLineSpacing)2405     public int getLineBottom(int line, boolean includeLineSpacing) {
2406         if (includeLineSpacing) {
2407             return getLineTop(line + 1);
2408         } else {
2409             return getLineTop(line + 1) - getLineExtra(line);
2410         }
2411     }
2412 
2413     /**
2414      * Return the vertical position of the baseline of the specified line.
2415      */
getLineBaseline(int line)2416     public final int getLineBaseline(int line) {
2417         // getLineTop(line+1) == getLineBottom(line)
2418         return getLineTop(line+1) - getLineDescent(line);
2419     }
2420 
2421     /**
2422      * Get the ascent of the text on the specified line.
2423      * The return value is negative to match the Paint.ascent() convention.
2424      */
getLineAscent(int line)2425     public final int getLineAscent(int line) {
2426         // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line)
2427         return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line));
2428     }
2429 
2430     /**
2431      * Return the extra space added as a result of line spacing attributes
2432      * {@link #getSpacingAdd()} and {@link #getSpacingMultiplier()}. Default value is {@code zero}.
2433      *
2434      * @param line the index of the line, the value should be equal or greater than {@code zero}
2435      * @hide
2436      */
getLineExtra(@ntRangefrom = 0) int line)2437     public int getLineExtra(@IntRange(from = 0) int line) {
2438         return 0;
2439     }
2440 
getOffsetToLeftOf(int offset)2441     public int getOffsetToLeftOf(int offset) {
2442         return getOffsetToLeftRightOf(offset, true);
2443     }
2444 
getOffsetToRightOf(int offset)2445     public int getOffsetToRightOf(int offset) {
2446         return getOffsetToLeftRightOf(offset, false);
2447     }
2448 
getOffsetToLeftRightOf(int caret, boolean toLeft)2449     private int getOffsetToLeftRightOf(int caret, boolean toLeft) {
2450         int line = getLineForOffset(caret);
2451         int lineStart = getLineStart(line);
2452         int lineEnd = getLineEnd(line);
2453         int lineDir = getParagraphDirection(line);
2454 
2455         boolean lineChanged = false;
2456         boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT);
2457         // if walking off line, look at the line we're headed to
2458         if (advance) {
2459             if (caret == lineEnd) {
2460                 if (line < getLineCount() - 1) {
2461                     lineChanged = true;
2462                     ++line;
2463                 } else {
2464                     return caret; // at very end, don't move
2465                 }
2466             }
2467         } else {
2468             if (caret == lineStart) {
2469                 if (line > 0) {
2470                     lineChanged = true;
2471                     --line;
2472                 } else {
2473                     return caret; // at very start, don't move
2474                 }
2475             }
2476         }
2477 
2478         if (lineChanged) {
2479             lineStart = getLineStart(line);
2480             lineEnd = getLineEnd(line);
2481             int newDir = getParagraphDirection(line);
2482             if (newDir != lineDir) {
2483                 // unusual case.  we want to walk onto the line, but it runs
2484                 // in a different direction than this one, so we fake movement
2485                 // in the opposite direction.
2486                 toLeft = !toLeft;
2487                 lineDir = newDir;
2488             }
2489         }
2490 
2491         Directions directions = getLineDirections(line);
2492 
2493         TextLine tl = TextLine.obtain();
2494         // XXX: we don't care about tabs
2495         tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null,
2496                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
2497                 isFallbackLineSpacingEnabled());
2498         caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft);
2499         TextLine.recycle(tl);
2500         return caret;
2501     }
2502 
getOffsetAtStartOf(int offset)2503     private int getOffsetAtStartOf(int offset) {
2504         // XXX this probably should skip local reorderings and
2505         // zero-width characters, look at callers
2506         if (offset == 0)
2507             return 0;
2508 
2509         CharSequence text = mText;
2510         char c = text.charAt(offset);
2511 
2512         if (c >= '\uDC00' && c <= '\uDFFF') {
2513             char c1 = text.charAt(offset - 1);
2514 
2515             if (c1 >= '\uD800' && c1 <= '\uDBFF')
2516                 offset -= 1;
2517         }
2518 
2519         if (mSpannedText) {
2520             ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
2521                                                        ReplacementSpan.class);
2522 
2523             for (int i = 0; i < spans.length; i++) {
2524                 int start = ((Spanned) text).getSpanStart(spans[i]);
2525                 int end = ((Spanned) text).getSpanEnd(spans[i]);
2526 
2527                 if (start < offset && end > offset)
2528                     offset = start;
2529             }
2530         }
2531 
2532         return offset;
2533     }
2534 
2535     /**
2536      * Determine whether we should clamp cursor position. Currently it's
2537      * only robust for left-aligned displays.
2538      * @hide
2539      */
2540     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
shouldClampCursor(int line)2541     public boolean shouldClampCursor(int line) {
2542         // Only clamp cursor position in left-aligned displays.
2543         switch (getParagraphAlignment(line)) {
2544             case ALIGN_LEFT:
2545                 return true;
2546             case ALIGN_NORMAL:
2547                 return getParagraphDirection(line) > 0;
2548             default:
2549                 return false;
2550         }
2551 
2552     }
2553 
2554     /**
2555      * Fills in the specified Path with a representation of a cursor
2556      * at the specified offset.  This will often be a vertical line
2557      * but can be multiple discontinuous lines in text with multiple
2558      * directionalities.
2559      */
getCursorPath(final int point, final Path dest, final CharSequence editingBuffer)2560     public void getCursorPath(final int point, final Path dest, final CharSequence editingBuffer) {
2561         dest.reset();
2562 
2563         int line = getLineForOffset(point);
2564         int top = getLineTop(line);
2565         int bottom = getLineBottom(line, /* includeLineSpacing= */ false);
2566 
2567         boolean clamped = shouldClampCursor(line);
2568         float h1 = getPrimaryHorizontal(point, clamped) - 0.5f;
2569 
2570         int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) |
2571                    TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING);
2572         int fn = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_ALT_ON);
2573         int dist = 0;
2574 
2575         if (caps != 0 || fn != 0) {
2576             dist = (bottom - top) >> 2;
2577 
2578             if (fn != 0)
2579                 top += dist;
2580             if (caps != 0)
2581                 bottom -= dist;
2582         }
2583 
2584         if (h1 < 0.5f)
2585             h1 = 0.5f;
2586 
2587         dest.moveTo(h1, top);
2588         dest.lineTo(h1, bottom);
2589 
2590         if (caps == 2) {
2591             dest.moveTo(h1, bottom);
2592             dest.lineTo(h1 - dist, bottom + dist);
2593             dest.lineTo(h1, bottom);
2594             dest.lineTo(h1 + dist, bottom + dist);
2595         } else if (caps == 1) {
2596             dest.moveTo(h1, bottom);
2597             dest.lineTo(h1 - dist, bottom + dist);
2598 
2599             dest.moveTo(h1 - dist, bottom + dist - 0.5f);
2600             dest.lineTo(h1 + dist, bottom + dist - 0.5f);
2601 
2602             dest.moveTo(h1 + dist, bottom + dist);
2603             dest.lineTo(h1, bottom);
2604         }
2605 
2606         if (fn == 2) {
2607             dest.moveTo(h1, top);
2608             dest.lineTo(h1 - dist, top - dist);
2609             dest.lineTo(h1, top);
2610             dest.lineTo(h1 + dist, top - dist);
2611         } else if (fn == 1) {
2612             dest.moveTo(h1, top);
2613             dest.lineTo(h1 - dist, top - dist);
2614 
2615             dest.moveTo(h1 - dist, top - dist + 0.5f);
2616             dest.lineTo(h1 + dist, top - dist + 0.5f);
2617 
2618             dest.moveTo(h1 + dist, top - dist);
2619             dest.lineTo(h1, top);
2620         }
2621     }
2622 
addSelection(int line, int start, int end, int top, int bottom, SelectionRectangleConsumer consumer)2623     private void addSelection(int line, int start, int end,
2624             int top, int bottom, SelectionRectangleConsumer consumer) {
2625         int linestart = getLineStart(line);
2626         int lineend = getLineEnd(line);
2627         Directions dirs = getLineDirections(line);
2628 
2629         if (lineend > linestart && mText.charAt(lineend - 1) == '\n') {
2630             lineend--;
2631         }
2632 
2633         for (int i = 0; i < dirs.mDirections.length; i += 2) {
2634             int here = linestart + dirs.mDirections[i];
2635             int there = here + (dirs.mDirections[i + 1] & RUN_LENGTH_MASK);
2636 
2637             if (there > lineend) {
2638                 there = lineend;
2639             }
2640 
2641             if (start <= there && end >= here) {
2642                 int st = Math.max(start, here);
2643                 int en = Math.min(end, there);
2644 
2645                 if (st != en) {
2646                     float h1 = getHorizontal(st, false, line, false /* not clamped */);
2647                     float h2 = getHorizontal(en, true, line, false /* not clamped */);
2648 
2649                     float left = Math.min(h1, h2);
2650                     float right = Math.max(h1, h2);
2651 
2652                     final @TextSelectionLayout int layout =
2653                             ((dirs.mDirections[i + 1] & RUN_RTL_FLAG) != 0)
2654                                     ? TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT
2655                                     : TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT;
2656 
2657                     consumer.accept(left, top, right, bottom, layout);
2658                 }
2659             }
2660         }
2661     }
2662 
2663     /**
2664      * Fills in the specified Path with a representation of a highlight
2665      * between the specified offsets.  This will often be a rectangle
2666      * or a potentially discontinuous set of rectangles.  If the start
2667      * and end are the same, the returned path is empty.
2668      */
getSelectionPath(int start, int end, Path dest)2669     public void getSelectionPath(int start, int end, Path dest) {
2670         dest.reset();
2671         getSelection(start, end, (left, top, right, bottom, textSelectionLayout) ->
2672                 dest.addRect(left, top, right, bottom, Path.Direction.CW));
2673     }
2674 
2675     /**
2676      * Calculates the rectangles which should be highlighted to indicate a selection between start
2677      * and end and feeds them into the given {@link SelectionRectangleConsumer}.
2678      *
2679      * @param start    the starting index of the selection
2680      * @param end      the ending index of the selection
2681      * @param consumer the {@link SelectionRectangleConsumer} which will receive the generated
2682      *                 rectangles. It will be called every time a rectangle is generated.
2683      * @hide
2684      * @see #getSelectionPath(int, int, Path)
2685      */
getSelection(int start, int end, final SelectionRectangleConsumer consumer)2686     public final void getSelection(int start, int end, final SelectionRectangleConsumer consumer) {
2687         if (start == end) {
2688             return;
2689         }
2690 
2691         if (end < start) {
2692             int temp = end;
2693             end = start;
2694             start = temp;
2695         }
2696 
2697         final int startline = getLineForOffset(start);
2698         final int endline = getLineForOffset(end);
2699 
2700         int top = getLineTop(startline);
2701         int bottom = getLineBottom(endline, /* includeLineSpacing= */ false);
2702 
2703         if (startline == endline) {
2704             addSelection(startline, start, end, top, bottom, consumer);
2705         } else {
2706             final float width = mWidth;
2707 
2708             addSelection(startline, start, getLineEnd(startline),
2709                     top, getLineBottom(startline), consumer);
2710 
2711             if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) {
2712                 consumer.accept(getLineLeft(startline), top, 0, getLineBottom(startline),
2713                         TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT);
2714             } else {
2715                 consumer.accept(getLineRight(startline), top, width, getLineBottom(startline),
2716                         TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT);
2717             }
2718 
2719             for (int i = startline + 1; i < endline; i++) {
2720                 top = getLineTop(i);
2721                 bottom = getLineBottom(i);
2722                 if (getParagraphDirection(i) == DIR_RIGHT_TO_LEFT) {
2723                     consumer.accept(0, top, width, bottom, TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT);
2724                 } else {
2725                     consumer.accept(0, top, width, bottom, TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT);
2726                 }
2727             }
2728 
2729             top = getLineTop(endline);
2730             bottom = getLineBottom(endline, /* includeLineSpacing= */ false);
2731 
2732             addSelection(endline, getLineStart(endline), end, top, bottom, consumer);
2733 
2734             if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT) {
2735                 consumer.accept(width, top, getLineRight(endline), bottom,
2736                         TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT);
2737             } else {
2738                 consumer.accept(0, top, getLineLeft(endline), bottom,
2739                         TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT);
2740             }
2741         }
2742     }
2743 
2744     /**
2745      * Get the alignment of the specified paragraph, taking into account
2746      * markup attached to it.
2747      */
getParagraphAlignment(int line)2748     public final Alignment getParagraphAlignment(int line) {
2749         Alignment align = mAlignment;
2750 
2751         if (mSpannedText) {
2752             Spanned sp = (Spanned) mText;
2753             AlignmentSpan[] spans = getParagraphSpans(sp, getLineStart(line),
2754                                                 getLineEnd(line),
2755                                                 AlignmentSpan.class);
2756 
2757             int spanLength = spans.length;
2758             if (spanLength > 0) {
2759                 align = spans[spanLength-1].getAlignment();
2760             }
2761         }
2762 
2763         return align;
2764     }
2765 
2766     /**
2767      * Get the left edge of the specified paragraph, inset by left margins.
2768      */
getParagraphLeft(int line)2769     public final int getParagraphLeft(int line) {
2770         int left = 0;
2771         int dir = getParagraphDirection(line);
2772         if (dir == DIR_RIGHT_TO_LEFT || !mSpannedText) {
2773             return left; // leading margin has no impact, or no styles
2774         }
2775         return getParagraphLeadingMargin(line);
2776     }
2777 
2778     /**
2779      * Get the right edge of the specified paragraph, inset by right margins.
2780      */
getParagraphRight(int line)2781     public final int getParagraphRight(int line) {
2782         int right = mWidth;
2783         int dir = getParagraphDirection(line);
2784         if (dir == DIR_LEFT_TO_RIGHT || !mSpannedText) {
2785             return right; // leading margin has no impact, or no styles
2786         }
2787         return right - getParagraphLeadingMargin(line);
2788     }
2789 
2790     /**
2791      * Returns the effective leading margin (unsigned) for this line,
2792      * taking into account LeadingMarginSpan and LeadingMarginSpan2.
2793      * @param line the line index
2794      * @return the leading margin of this line
2795      */
getParagraphLeadingMargin(int line)2796     private int getParagraphLeadingMargin(int line) {
2797         if (!mSpannedText) {
2798             return 0;
2799         }
2800         Spanned spanned = (Spanned) mText;
2801 
2802         int lineStart = getLineStart(line);
2803         int lineEnd = getLineEnd(line);
2804         int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd,
2805                 LeadingMarginSpan.class);
2806         LeadingMarginSpan[] spans = getParagraphSpans(spanned, lineStart, spanEnd,
2807                                                 LeadingMarginSpan.class);
2808         if (spans.length == 0) {
2809             return 0; // no leading margin span;
2810         }
2811 
2812         int margin = 0;
2813 
2814         boolean useFirstLineMargin = lineStart == 0 || spanned.charAt(lineStart - 1) == '\n';
2815         for (int i = 0; i < spans.length; i++) {
2816             if (spans[i] instanceof LeadingMarginSpan2) {
2817                 int spStart = spanned.getSpanStart(spans[i]);
2818                 int spanLine = getLineForOffset(spStart);
2819                 int count = ((LeadingMarginSpan2) spans[i]).getLeadingMarginLineCount();
2820                 // if there is more than one LeadingMarginSpan2, use the count that is greatest
2821                 useFirstLineMargin |= line < spanLine + count;
2822             }
2823         }
2824         for (int i = 0; i < spans.length; i++) {
2825             LeadingMarginSpan span = spans[i];
2826             margin += span.getLeadingMargin(useFirstLineMargin);
2827         }
2828 
2829         return margin;
2830     }
2831 
2832     private static float measurePara(TextPaint paint, CharSequence text, int start, int end,
2833             TextDirectionHeuristic textDir) {
2834         MeasuredParagraph mt = null;
2835         TextLine tl = TextLine.obtain();
2836         try {
2837             mt = MeasuredParagraph.buildForBidi(text, start, end, textDir, mt);
2838             final char[] chars = mt.getChars();
2839             final int len = chars.length;
2840             final Directions directions = mt.getDirections(0, len);
2841             final int dir = mt.getParagraphDir();
2842             boolean hasTabs = false;
2843             TabStops tabStops = null;
2844             // leading margins should be taken into account when measuring a paragraph
2845             int margin = 0;
2846             if (text instanceof Spanned) {
2847                 Spanned spanned = (Spanned) text;
2848                 LeadingMarginSpan[] spans = getParagraphSpans(spanned, start, end,
2849                         LeadingMarginSpan.class);
2850                 for (LeadingMarginSpan lms : spans) {
2851                     margin += lms.getLeadingMargin(true);
2852                 }
2853             }
2854             for (int i = 0; i < len; ++i) {
2855                 if (chars[i] == '\t') {
2856                     hasTabs = true;
2857                     if (text instanceof Spanned) {
2858                         Spanned spanned = (Spanned) text;
2859                         int spanEnd = spanned.nextSpanTransition(start, end,
2860                                 TabStopSpan.class);
2861                         TabStopSpan[] spans = getParagraphSpans(spanned, start, spanEnd,
2862                                 TabStopSpan.class);
2863                         if (spans.length > 0) {
2864                             tabStops = new TabStops(TAB_INCREMENT, spans);
2865                         }
2866                     }
2867                     break;
2868                 }
2869             }
2870             tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops,
2871                     0 /* ellipsisStart */, 0 /* ellipsisEnd */,
2872                     false /* use fallback line spacing. unused */);
2873             return margin + Math.abs(tl.metrics(null));
2874         } finally {
2875             TextLine.recycle(tl);
2876             if (mt != null) {
2877                 mt.recycle();
2878             }
2879         }
2880     }
2881 
2882     /**
2883      * @hide
2884      */
2885     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
2886     public static class TabStops {
2887         private float[] mStops;
2888         private int mNumStops;
2889         private float mIncrement;
2890 
2891         public TabStops(float increment, Object[] spans) {
2892             reset(increment, spans);
2893         }
2894 
2895         void reset(float increment, Object[] spans) {
2896             this.mIncrement = increment;
2897 
2898             int ns = 0;
2899             if (spans != null) {
2900                 float[] stops = this.mStops;
2901                 for (Object o : spans) {
2902                     if (o instanceof TabStopSpan) {
2903                         if (stops == null) {
2904                             stops = new float[10];
2905                         } else if (ns == stops.length) {
2906                             float[] nstops = new float[ns * 2];
2907                             for (int i = 0; i < ns; ++i) {
2908                                 nstops[i] = stops[i];
2909                             }
2910                             stops = nstops;
2911                         }
2912                         stops[ns++] = ((TabStopSpan) o).getTabStop();
2913                     }
2914                 }
2915                 if (ns > 1) {
2916                     Arrays.sort(stops, 0, ns);
2917                 }
2918                 if (stops != this.mStops) {
2919                     this.mStops = stops;
2920                 }
2921             }
2922             this.mNumStops = ns;
2923         }
2924 
2925         float nextTab(float h) {
2926             int ns = this.mNumStops;
2927             if (ns > 0) {
2928                 float[] stops = this.mStops;
2929                 for (int i = 0; i < ns; ++i) {
2930                     float stop = stops[i];
2931                     if (stop > h) {
2932                         return stop;
2933                     }
2934                 }
2935             }
2936             return nextDefaultStop(h, mIncrement);
2937         }
2938 
2939         /**
2940          * Returns the position of next tab stop.
2941          */
2942         public static float nextDefaultStop(float h, float inc) {
2943             return ((int) ((h + inc) / inc)) * inc;
2944         }
2945     }
2946 
2947     /**
2948      * Returns the position of the next tab stop after h on the line.
2949      *
2950      * @param text the text
2951      * @param start start of the line
2952      * @param end limit of the line
2953      * @param h the current horizontal offset
2954      * @param tabs the tabs, can be null.  If it is null, any tabs in effect
2955      * on the line will be used.  If there are no tabs, a default offset
2956      * will be used to compute the tab stop.
2957      * @return the offset of the next tab stop.
2958      */
2959     /* package */ static float nextTab(CharSequence text, int start, int end,
2960                                        float h, Object[] tabs) {
2961         float nh = Float.MAX_VALUE;
2962         boolean alltabs = false;
2963 
2964         if (text instanceof Spanned) {
2965             if (tabs == null) {
2966                 tabs = getParagraphSpans((Spanned) text, start, end, TabStopSpan.class);
2967                 alltabs = true;
2968             }
2969 
2970             for (int i = 0; i < tabs.length; i++) {
2971                 if (!alltabs) {
2972                     if (!(tabs[i] instanceof TabStopSpan))
2973                         continue;
2974                 }
2975 
2976                 int where = ((TabStopSpan) tabs[i]).getTabStop();
2977 
2978                 if (where < nh && where > h)
2979                     nh = where;
2980             }
2981 
2982             if (nh != Float.MAX_VALUE)
2983                 return nh;
2984         }
2985 
2986         return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT;
2987     }
2988 
2989     protected final boolean isSpanned() {
2990         return mSpannedText;
2991     }
2992 
2993     /**
2994      * Returns the same as <code>text.getSpans()</code>, except where
2995      * <code>start</code> and <code>end</code> are the same and are not
2996      * at the very beginning of the text, in which case an empty array
2997      * is returned instead.
2998      * <p>
2999      * This is needed because of the special case that <code>getSpans()</code>
3000      * on an empty range returns the spans adjacent to that range, which is
3001      * primarily for the sake of <code>TextWatchers</code> so they will get
3002      * notifications when text goes from empty to non-empty.  But it also
3003      * has the unfortunate side effect that if the text ends with an empty
3004      * paragraph, that paragraph accidentally picks up the styles of the
3005      * preceding paragraph (even though those styles will not be picked up
3006      * by new text that is inserted into the empty paragraph).
3007      * <p>
3008      * The reason it just checks whether <code>start</code> and <code>end</code>
3009      * is the same is that the only time a line can contain 0 characters
3010      * is if it is the final paragraph of the Layout; otherwise any line will
3011      * contain at least one printing or newline character.  The reason for the
3012      * additional check if <code>start</code> is greater than 0 is that
3013      * if the empty paragraph is the entire content of the buffer, paragraph
3014      * styles that are already applied to the buffer will apply to text that
3015      * is inserted into it.
3016      */
3017     /* package */static <T> T[] getParagraphSpans(Spanned text, int start, int end, Class<T> type) {
3018         if (start == end && start > 0) {
3019             return ArrayUtils.emptyArray(type);
3020         }
3021 
3022         if(text instanceof SpannableStringBuilder) {
3023             return ((SpannableStringBuilder) text).getSpans(start, end, type, false);
3024         } else {
3025             return text.getSpans(start, end, type);
3026         }
3027     }
3028 
3029     private void ellipsize(int start, int end, int line,
3030                            char[] dest, int destoff, TextUtils.TruncateAt method) {
3031         final int ellipsisCount = getEllipsisCount(line);
3032         if (ellipsisCount == 0) {
3033             return;
3034         }
3035         final int ellipsisStart = getEllipsisStart(line);
3036         final int lineStart = getLineStart(line);
3037 
3038         final String ellipsisString = TextUtils.getEllipsisString(method);
3039         final int ellipsisStringLen = ellipsisString.length();
3040         // Use the ellipsis string only if there are that at least as many characters to replace.
3041         final boolean useEllipsisString = ellipsisCount >= ellipsisStringLen;
3042         final int min = Math.max(0, start - ellipsisStart - lineStart);
3043         final int max = Math.min(ellipsisCount, end - ellipsisStart - lineStart);
3044 
3045         for (int i = min; i < max; i++) {
3046             final char c;
3047             if (useEllipsisString && i < ellipsisStringLen) {
3048                 c = ellipsisString.charAt(i);
3049             } else {
3050                 c = TextUtils.ELLIPSIS_FILLER;
3051             }
3052 
3053             final int a = i + ellipsisStart + lineStart;
3054             dest[destoff + a - start] = c;
3055         }
3056     }
3057 
3058     /**
3059      * Stores information about bidirectional (left-to-right or right-to-left)
3060      * text within the layout of a line.
3061      */
3062     public static class Directions {
3063         /**
3064          * Directions represents directional runs within a line of text. Runs are pairs of ints
3065          * listed in visual order, starting from the leading margin.  The first int of each pair is
3066          * the offset from the first character of the line to the start of the run.  The second int
3067          * represents both the length and level of the run. The length is in the lower bits,
3068          * accessed by masking with RUN_LENGTH_MASK.  The level is in the higher bits, accessed by
3069          * shifting by RUN_LEVEL_SHIFT and masking by RUN_LEVEL_MASK. To simply test for an RTL
3070          * direction, test the bit using RUN_RTL_FLAG, if set then the direction is rtl.
3071          * @hide
3072          */
3073         @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
3074         public int[] mDirections;
3075 
3076         /**
3077          * @hide
3078          */
3079         @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
Directions(int[] dirs)3080         public Directions(int[] dirs) {
3081             mDirections = dirs;
3082         }
3083 
3084         /**
3085          * Returns number of BiDi runs.
3086          *
3087          * @hide
3088          */
getRunCount()3089         public @IntRange(from = 0) int getRunCount() {
3090             return mDirections.length / 2;
3091         }
3092 
3093         /**
3094          * Returns the start offset of the BiDi run.
3095          *
3096          * @param runIndex the index of the BiDi run
3097          * @return the start offset of the BiDi run.
3098          * @hide
3099          */
getRunStart(@ntRangefrom = 0) int runIndex)3100         public @IntRange(from = 0) int getRunStart(@IntRange(from = 0) int runIndex) {
3101             return mDirections[runIndex * 2];
3102         }
3103 
3104         /**
3105          * Returns the length of the BiDi run.
3106          *
3107          * Note that this method may return too large number due to reducing the number of object
3108          * allocations. The too large number means the remaining part is assigned to this run. The
3109          * caller must clamp the returned value.
3110          *
3111          * @param runIndex the index of the BiDi run
3112          * @return the length of the BiDi run.
3113          * @hide
3114          */
getRunLength(@ntRangefrom = 0) int runIndex)3115         public @IntRange(from = 0) int getRunLength(@IntRange(from = 0) int runIndex) {
3116             return mDirections[runIndex * 2 + 1] & RUN_LENGTH_MASK;
3117         }
3118 
3119         /**
3120          * Returns the BiDi level of this run.
3121          *
3122          * @param runIndex the index of the BiDi run
3123          * @return the BiDi level of this run.
3124          * @hide
3125          */
3126         @IntRange(from = 0)
getRunLevel(int runIndex)3127         public int getRunLevel(int runIndex) {
3128             return (mDirections[runIndex * 2 + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
3129         }
3130 
3131         /**
3132          * Returns true if the BiDi run is RTL.
3133          *
3134          * @param runIndex the index of the BiDi run
3135          * @return true if the BiDi run is RTL.
3136          * @hide
3137          */
isRunRtl(int runIndex)3138         public boolean isRunRtl(int runIndex) {
3139             return (mDirections[runIndex * 2 + 1] & RUN_RTL_FLAG) != 0;
3140         }
3141     }
3142 
3143     /**
3144      * Return the offset of the first character to be ellipsized away,
3145      * relative to the start of the line.  (So 0 if the beginning of the
3146      * line is ellipsized, not getLineStart().)
3147      */
3148     public abstract int getEllipsisStart(int line);
3149 
3150     /**
3151      * Returns the number of characters to be ellipsized away, or 0 if
3152      * no ellipsis is to take place.
3153      */
3154     public abstract int getEllipsisCount(int line);
3155 
3156     /* package */ static class Ellipsizer implements CharSequence, GetChars {
3157         /* package */ CharSequence mText;
3158         /* package */ Layout mLayout;
3159         /* package */ int mWidth;
3160         /* package */ TextUtils.TruncateAt mMethod;
3161 
Ellipsizer(CharSequence s)3162         public Ellipsizer(CharSequence s) {
3163             mText = s;
3164         }
3165 
charAt(int off)3166         public char charAt(int off) {
3167             char[] buf = TextUtils.obtain(1);
3168             getChars(off, off + 1, buf, 0);
3169             char ret = buf[0];
3170 
3171             TextUtils.recycle(buf);
3172             return ret;
3173         }
3174 
getChars(int start, int end, char[] dest, int destoff)3175         public void getChars(int start, int end, char[] dest, int destoff) {
3176             int line1 = mLayout.getLineForOffset(start);
3177             int line2 = mLayout.getLineForOffset(end);
3178 
3179             TextUtils.getChars(mText, start, end, dest, destoff);
3180 
3181             for (int i = line1; i <= line2; i++) {
3182                 mLayout.ellipsize(start, end, i, dest, destoff, mMethod);
3183             }
3184         }
3185 
length()3186         public int length() {
3187             return mText.length();
3188         }
3189 
subSequence(int start, int end)3190         public CharSequence subSequence(int start, int end) {
3191             char[] s = new char[end - start];
3192             getChars(start, end, s, 0);
3193             return new String(s);
3194         }
3195 
3196         @Override
toString()3197         public String toString() {
3198             char[] s = new char[length()];
3199             getChars(0, length(), s, 0);
3200             return new String(s);
3201         }
3202 
3203     }
3204 
3205     /* package */ static class SpannedEllipsizer extends Ellipsizer implements Spanned {
3206         private Spanned mSpanned;
3207 
SpannedEllipsizer(CharSequence display)3208         public SpannedEllipsizer(CharSequence display) {
3209             super(display);
3210             mSpanned = (Spanned) display;
3211         }
3212 
getSpans(int start, int end, Class<T> type)3213         public <T> T[] getSpans(int start, int end, Class<T> type) {
3214             return mSpanned.getSpans(start, end, type);
3215         }
3216 
getSpanStart(Object tag)3217         public int getSpanStart(Object tag) {
3218             return mSpanned.getSpanStart(tag);
3219         }
3220 
getSpanEnd(Object tag)3221         public int getSpanEnd(Object tag) {
3222             return mSpanned.getSpanEnd(tag);
3223         }
3224 
getSpanFlags(Object tag)3225         public int getSpanFlags(Object tag) {
3226             return mSpanned.getSpanFlags(tag);
3227         }
3228 
3229         @SuppressWarnings("rawtypes")
nextSpanTransition(int start, int limit, Class type)3230         public int nextSpanTransition(int start, int limit, Class type) {
3231             return mSpanned.nextSpanTransition(start, limit, type);
3232         }
3233 
3234         @Override
subSequence(int start, int end)3235         public CharSequence subSequence(int start, int end) {
3236             char[] s = new char[end - start];
3237             getChars(start, end, s, 0);
3238 
3239             SpannableString ss = new SpannableString(new String(s));
3240             TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0);
3241             return ss;
3242         }
3243     }
3244 
3245     private CharSequence mText;
3246     @UnsupportedAppUsage
3247     private TextPaint mPaint;
3248     private TextPaint mWorkPaint = new TextPaint();
3249     private int mWidth;
3250     private Alignment mAlignment = Alignment.ALIGN_NORMAL;
3251     private float mSpacingMult;
3252     private float mSpacingAdd;
3253     private static final Rect sTempRect = new Rect();
3254     private boolean mSpannedText;
3255     private TextDirectionHeuristic mTextDir;
3256     private SpanSet<LineBackgroundSpan> mLineBackgroundSpans;
3257     private int mJustificationMode;
3258 
3259     /** @hide */
3260     @IntDef(prefix = { "DIR_" }, value = {
3261             DIR_LEFT_TO_RIGHT,
3262             DIR_RIGHT_TO_LEFT
3263     })
3264     @Retention(RetentionPolicy.SOURCE)
3265     public @interface Direction {}
3266 
3267     public static final int DIR_LEFT_TO_RIGHT = 1;
3268     public static final int DIR_RIGHT_TO_LEFT = -1;
3269 
3270     /* package */ static final int DIR_REQUEST_LTR = 1;
3271     /* package */ static final int DIR_REQUEST_RTL = -1;
3272     @UnsupportedAppUsage
3273     /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2;
3274     /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2;
3275 
3276     /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff;
3277     /* package */ static final int RUN_LEVEL_SHIFT = 26;
3278     /* package */ static final int RUN_LEVEL_MASK = 0x3f;
3279     /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT;
3280 
3281     public enum Alignment {
3282         ALIGN_NORMAL,
3283         ALIGN_OPPOSITE,
3284         ALIGN_CENTER,
3285         /** @hide */
3286         @UnsupportedAppUsage
3287         ALIGN_LEFT,
3288         /** @hide */
3289         @UnsupportedAppUsage
3290         ALIGN_RIGHT,
3291     }
3292 
3293     private static final float TAB_INCREMENT = 20;
3294 
3295     /** @hide */
3296     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
3297     @UnsupportedAppUsage
3298     public static final Directions DIRS_ALL_LEFT_TO_RIGHT =
3299         new Directions(new int[] { 0, RUN_LENGTH_MASK });
3300 
3301     /** @hide */
3302     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
3303     @UnsupportedAppUsage
3304     public static final Directions DIRS_ALL_RIGHT_TO_LEFT =
3305         new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG });
3306 
3307     /** @hide */
3308     @Retention(RetentionPolicy.SOURCE)
3309     @IntDef(prefix = { "TEXT_SELECTION_LAYOUT_" }, value = {
3310             TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT,
3311             TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT
3312     })
3313     public @interface TextSelectionLayout {}
3314 
3315     /** @hide */
3316     public static final int TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT = 0;
3317     /** @hide */
3318     public static final int TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT = 1;
3319 
3320     /** @hide */
3321     @FunctionalInterface
3322     public interface SelectionRectangleConsumer {
3323         /**
3324          * Performs this operation on the given rectangle.
3325          *
3326          * @param left   the left edge of the rectangle
3327          * @param top    the top edge of the rectangle
3328          * @param right  the right edge of the rectangle
3329          * @param bottom the bottom edge of the rectangle
3330          * @param textSelectionLayout the layout (RTL or LTR) of the text covered by this
3331          *                            selection rectangle
3332          */
3333         void accept(float left, float top, float right, float bottom,
3334                 @TextSelectionLayout int textSelectionLayout);
3335     }
3336 
3337     /**
3338      * Strategy for determining whether a text segment is inside a rectangle area.
3339      *
3340      * @see #getRangeForRect(RectF, SegmentFinder, TextInclusionStrategy)
3341      */
3342     @FunctionalInterface
3343     public interface TextInclusionStrategy {
3344         /**
3345          * Returns true if this {@link TextInclusionStrategy} considers the segment with bounds
3346          * {@code segmentBounds} to be inside {@code area}.
3347          *
3348          * <p>The segment is a range of text which does not cross line boundaries or directional run
3349          * boundaries. The horizontal bounds of the segment are the start bound of the first
3350          * character to the end bound of the last character. The vertical bounds match the line
3351          * bounds ({@code getLineTop(line)} and {@code getLineBottom(line, false)}).
3352          */
3353         boolean isSegmentInside(@NonNull RectF segmentBounds, @NonNull RectF area);
3354     }
3355 }
3356