• 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 static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
20 import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION;
21 import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
22 
23 import android.annotation.ColorInt;
24 import android.annotation.FlaggedApi;
25 import android.annotation.FloatRange;
26 import android.annotation.IntDef;
27 import android.annotation.IntRange;
28 import android.annotation.NonNull;
29 import android.annotation.Nullable;
30 import android.annotation.SuppressLint;
31 import android.compat.annotation.UnsupportedAppUsage;
32 import android.graphics.BlendMode;
33 import android.graphics.Canvas;
34 import android.graphics.Color;
35 import android.graphics.Paint;
36 import android.graphics.Path;
37 import android.graphics.Rect;
38 import android.graphics.RectF;
39 import android.graphics.text.LineBreakConfig;
40 import android.graphics.text.LineBreaker;
41 import android.os.Build;
42 import android.text.method.TextKeyListener;
43 import android.text.style.AlignmentSpan;
44 import android.text.style.LeadingMarginSpan;
45 import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
46 import android.text.style.LineBackgroundSpan;
47 import android.text.style.ParagraphStyle;
48 import android.text.style.ReplacementSpan;
49 import android.text.style.TabStopSpan;
50 import android.widget.TextView;
51 
52 import com.android.graphics.hwui.flags.Flags;
53 import com.android.internal.annotations.VisibleForTesting;
54 import com.android.internal.graphics.ColorUtils;
55 import com.android.internal.util.ArrayUtils;
56 import com.android.internal.util.GrowingArrayUtils;
57 
58 import java.lang.annotation.Retention;
59 import java.lang.annotation.RetentionPolicy;
60 import java.text.BreakIterator;
61 import java.util.Arrays;
62 import java.util.List;
63 import java.util.Locale;
64 
65 /**
66  * A base class that manages text layout in visual elements on
67  * the screen.
68  * <p>For text that will be edited, use a {@link DynamicLayout},
69  * which will be updated as the text changes.
70  * For text that will not change, use a {@link StaticLayout}.
71  */
72 @android.ravenwood.annotation.RavenwoodKeepWholeClass
73 public abstract class Layout {
74 
75     // These should match the constants in framework/base/libs/hwui/hwui/DrawTextFunctor.h
76     private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX = 0f;
77     private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR = 0f;
78     // since we're not using soft light yet, this needs to be much lower than the spec'd 0.8
79     private static final float HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE = 0.7f;
80     @VisibleForTesting
81     static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP = 5f;
82     @VisibleForTesting
83     static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR = 0.5f;
84 
85     /** @hide */
86     @IntDef(prefix = { "BREAK_STRATEGY_" }, value = {
87             LineBreaker.BREAK_STRATEGY_SIMPLE,
88             LineBreaker.BREAK_STRATEGY_HIGH_QUALITY,
89             LineBreaker.BREAK_STRATEGY_BALANCED
90     })
91     @Retention(RetentionPolicy.SOURCE)
92     public @interface BreakStrategy {}
93 
94     /**
95      * Value for break strategy indicating simple line breaking. Automatic hyphens are not added
96      * (though soft hyphens are respected), and modifying text generally doesn't affect the layout
97      * before it (which yields a more consistent user experience when editing), but layout may not
98      * be the highest quality.
99      */
100     public static final int BREAK_STRATEGY_SIMPLE = LineBreaker.BREAK_STRATEGY_SIMPLE;
101 
102     /**
103      * Value for break strategy indicating high quality line breaking, including automatic
104      * hyphenation and doing whole-paragraph optimization of line breaks.
105      */
106     public static final int BREAK_STRATEGY_HIGH_QUALITY = LineBreaker.BREAK_STRATEGY_HIGH_QUALITY;
107 
108     /**
109      * Value for break strategy indicating balanced line breaking. The breaks are chosen to
110      * make all lines as close to the same length as possible, including automatic hyphenation.
111      */
112     public static final int BREAK_STRATEGY_BALANCED = LineBreaker.BREAK_STRATEGY_BALANCED;
113 
114     /** @hide */
115     @IntDef(prefix = { "HYPHENATION_FREQUENCY_" }, value = {
116             HYPHENATION_FREQUENCY_NORMAL,
117             HYPHENATION_FREQUENCY_NORMAL_FAST,
118             HYPHENATION_FREQUENCY_FULL,
119             HYPHENATION_FREQUENCY_FULL_FAST,
120             HYPHENATION_FREQUENCY_NONE
121     })
122     @Retention(RetentionPolicy.SOURCE)
123     public @interface HyphenationFrequency {}
124 
125     /**
126      * Value for hyphenation frequency indicating no automatic hyphenation. Useful
127      * for backward compatibility, and for cases where the automatic hyphenation algorithm results
128      * in incorrect hyphenation. Mid-word breaks may still happen when a word is wider than the
129      * layout and there is otherwise no valid break. Soft hyphens are ignored and will not be used
130      * as suggestions for potential line breaks.
131      */
132     public static final int HYPHENATION_FREQUENCY_NONE = 0;
133 
134     /**
135      * Value for hyphenation frequency indicating a light amount of automatic hyphenation, which
136      * is a conservative default. Useful for informal cases, such as short sentences or chat
137      * messages.
138      */
139     public static final int HYPHENATION_FREQUENCY_NORMAL = 1;
140 
141     /**
142      * Value for hyphenation frequency indicating the full amount of automatic hyphenation, typical
143      * in typography. Useful for running text and where it's important to put the maximum amount of
144      * text in a screen with limited space.
145      */
146     public static final int HYPHENATION_FREQUENCY_FULL = 2;
147 
148     /**
149      * Value for hyphenation frequency indicating a light amount of automatic hyphenation with
150      * using faster algorithm.
151      *
152      * This option is useful for informal cases, such as short sentences or chat messages. To make
153      * text rendering faster with hyphenation, this algorithm ignores some hyphen character related
154      * typographic features, e.g. kerning.
155      */
156     public static final int HYPHENATION_FREQUENCY_NORMAL_FAST = 3;
157     /**
158      * Value for hyphenation frequency indicating the full amount of automatic hyphenation with
159      * using faster algorithm.
160      *
161      * This option is useful for running text and where it's important to put the maximum amount of
162      * text in a screen with limited space. To make text rendering faster with hyphenation, this
163      * algorithm ignores some hyphen character related typographic features, e.g. kerning.
164      */
165     public static final int HYPHENATION_FREQUENCY_FULL_FAST = 4;
166 
167     private static final ParagraphStyle[] NO_PARA_SPANS =
168         ArrayUtils.emptyArray(ParagraphStyle.class);
169 
170     /** @hide */
171     @IntDef(prefix = { "JUSTIFICATION_MODE_" }, value = {
172             LineBreaker.JUSTIFICATION_MODE_NONE,
173             LineBreaker.JUSTIFICATION_MODE_INTER_WORD,
174             LineBreaker.JUSTIFICATION_MODE_INTER_CHARACTER,
175     })
176     @Retention(RetentionPolicy.SOURCE)
177     public @interface JustificationMode {}
178 
179     /**
180      * Value for justification mode indicating no justification.
181      */
182     public static final int JUSTIFICATION_MODE_NONE = LineBreaker.JUSTIFICATION_MODE_NONE;
183 
184     /**
185      * Value for justification mode indicating the text is justified by stretching word spacing.
186      */
187     public static final int JUSTIFICATION_MODE_INTER_WORD =
188             LineBreaker.JUSTIFICATION_MODE_INTER_WORD;
189 
190     /**
191      * Value for justification mode indicating the text is justified by stretching letter spacing.
192      */
193     @FlaggedApi(FLAG_LETTER_SPACING_JUSTIFICATION)
194     public static final int JUSTIFICATION_MODE_INTER_CHARACTER =
195             LineBreaker.JUSTIFICATION_MODE_INTER_CHARACTER;
196 
197     /*
198      * Line spacing multiplier for default line spacing.
199      */
200     public static final float DEFAULT_LINESPACING_MULTIPLIER = 1.0f;
201 
202     /*
203      * Line spacing addition for default line spacing.
204      */
205     public static final float DEFAULT_LINESPACING_ADDITION = 0.0f;
206 
207     /**
208      * Strategy which considers a text segment to be inside a rectangle area if the segment bounds
209      * intersect the rectangle.
210      */
211     @NonNull
212     public static final TextInclusionStrategy INCLUSION_STRATEGY_ANY_OVERLAP =
213             RectF::intersects;
214 
215     /**
216      * Strategy which considers a text segment to be inside a rectangle area if the center of the
217      * segment bounds is inside the rectangle.
218      */
219     @NonNull
220     public static final TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_CENTER =
221             (segmentBounds, area) ->
222                     area.contains(segmentBounds.centerX(), segmentBounds.centerY());
223 
224     /**
225      * Strategy which considers a text segment to be inside a rectangle area if the segment bounds
226      * are completely contained within the rectangle.
227      */
228     @NonNull
229     public static final TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_ALL =
230             (segmentBounds, area) -> area.contains(segmentBounds);
231 
232     /**
233      * Return how wide a layout must be in order to display the specified text with one line per
234      * paragraph.
235      *
236      * <p>As of O, Uses
237      * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR} as the default text direction heuristics. In
238      * the earlier versions uses {@link TextDirectionHeuristics#LTR} as the default.</p>
239      */
getDesiredWidth(CharSequence source, TextPaint paint)240     public static float getDesiredWidth(CharSequence source,
241                                         TextPaint paint) {
242         return getDesiredWidth(source, 0, source.length(), paint);
243     }
244 
245     /**
246      * Return how wide a layout must be in order to display the specified text slice with one
247      * line per paragraph.
248      *
249      * <p>As of O, Uses
250      * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR} as the default text direction heuristics. In
251      * the earlier versions uses {@link TextDirectionHeuristics#LTR} as the default.</p>
252      */
getDesiredWidth(CharSequence source, int start, int end, TextPaint paint)253     public static float getDesiredWidth(CharSequence source, int start, int end, TextPaint paint) {
254         return getDesiredWidth(source, start, end, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR);
255     }
256 
257     /**
258      * Return how wide a layout must be in order to display the
259      * specified text slice with one line per paragraph.
260      *
261      * @hide
262      */
getDesiredWidth(CharSequence source, int start, int end, TextPaint paint, TextDirectionHeuristic textDir)263     public static float getDesiredWidth(CharSequence source, int start, int end, TextPaint paint,
264             TextDirectionHeuristic textDir) {
265         return getDesiredWidthWithLimit(source, start, end, paint, textDir, Float.MAX_VALUE, false);
266     }
267     /**
268      * Return how wide a layout must be in order to display the
269      * specified text slice with one line per paragraph.
270      *
271      * If the measured width exceeds given limit, returns limit value instead.
272      * @hide
273      */
getDesiredWidthWithLimit(CharSequence source, int start, int end, TextPaint paint, TextDirectionHeuristic textDir, float upperLimit, boolean useBoundsForWidth)274     public static float getDesiredWidthWithLimit(CharSequence source, int start, int end,
275             TextPaint paint, TextDirectionHeuristic textDir, float upperLimit,
276             boolean useBoundsForWidth) {
277         float need = 0;
278 
279         int next;
280         for (int i = start; i <= end; i = next) {
281             next = TextUtils.indexOf(source, '\n', i, end);
282 
283             if (next < 0)
284                 next = end;
285 
286             // note, omits trailing paragraph char
287             float w = measurePara(paint, source, i, next, textDir, useBoundsForWidth);
288             if (w > upperLimit) {
289                 return upperLimit;
290             }
291 
292             if (w > need)
293                 need = w;
294 
295             next++;
296         }
297 
298         return need;
299     }
300 
301     /**
302      * Subclasses of Layout use this constructor to set the display text,
303      * width, and other standard properties.
304      * @param text the text to render
305      * @param paint the default paint for the layout.  Styles can override
306      * various attributes of the paint.
307      * @param width the wrapping width for the text.
308      * @param align whether to left, right, or center the text.  Styles can
309      * override the alignment.
310      * @param spacingMult factor by which to scale the font size to get the
311      * default line spacing
312      * @param spacingAdd amount to add to the default line spacing
313      */
Layout(CharSequence text, TextPaint paint, int width, Alignment align, float spacingMult, float spacingAdd)314     protected Layout(CharSequence text, TextPaint paint,
315                      int width, Alignment align,
316                      float spacingMult, float spacingAdd) {
317         this(text, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
318                 spacingMult, spacingAdd, false, false, 0, null, Integer.MAX_VALUE,
319                 BREAK_STRATEGY_SIMPLE, HYPHENATION_FREQUENCY_NONE, null, null,
320                 JUSTIFICATION_MODE_NONE, LineBreakConfig.NONE, false, false, null);
321     }
322 
323     /**
324      * Subclasses of Layout use this constructor to set the display text,
325      * width, and other standard properties.
326      * @param text the text to render
327      * @param paint the default paint for the layout.  Styles can override
328      * various attributes of the paint.
329      * @param width the wrapping width for the text.
330      * @param align whether to left, right, or center the text.  Styles can
331      * override the alignment.
332      * @param textDir a text direction heuristic.
333      * @param spacingMult factor by which to scale the font size to get the
334      * default line spacing
335      * @param spacingAdd amount to add to the default line spacing
336      * @param includePad true for enabling including font padding
337      * @param fallbackLineSpacing true for enabling fallback line spacing
338      * @param ellipsizedWidth width as used for ellipsizing purpose
339      * @param ellipsize an ellipsize option
340      * @param maxLines a maximum number of lines.
341      * @param breakStrategy a break strategy.
342      * @param hyphenationFrequency a hyphenation frequency
343      * @param leftIndents a visually left margins
344      * @param rightIndents a visually right margins
345      * @param justificationMode a justification mode
346      * @param lineBreakConfig a line break config
347      *
348      * @hide
349      */
Layout( CharSequence text, TextPaint paint, int width, Alignment align, TextDirectionHeuristic textDir, float spacingMult, float spacingAdd, boolean includePad, boolean fallbackLineSpacing, int ellipsizedWidth, TextUtils.TruncateAt ellipsize, int maxLines, int breakStrategy, int hyphenationFrequency, int[] leftIndents, int[] rightIndents, int justificationMode, LineBreakConfig lineBreakConfig, boolean useBoundsForWidth, boolean shiftDrawingOffsetForStartOverhang, Paint.FontMetrics minimumFontMetrics )350     protected Layout(
351             CharSequence text,
352             TextPaint paint,
353             int width,
354             Alignment align,
355             TextDirectionHeuristic textDir,
356             float spacingMult,
357             float spacingAdd,
358             boolean includePad,
359             boolean fallbackLineSpacing,
360             int ellipsizedWidth,
361             TextUtils.TruncateAt ellipsize,
362             int maxLines,
363             int breakStrategy,
364             int hyphenationFrequency,
365             int[] leftIndents,
366             int[] rightIndents,
367             int justificationMode,
368             LineBreakConfig lineBreakConfig,
369             boolean useBoundsForWidth,
370             boolean shiftDrawingOffsetForStartOverhang,
371             Paint.FontMetrics minimumFontMetrics
372     ) {
373 
374         if (width < 0)
375             throw new IllegalArgumentException("Layout: " + width + " < 0");
376 
377         // Ensure paint doesn't have baselineShift set.
378         // While normally we don't modify the paint the user passed in,
379         // we were already doing this in Styled.drawUniformRun with both
380         // baselineShift and bgColor.  We probably should reevaluate bgColor.
381         if (paint != null) {
382             paint.bgColor = 0;
383             paint.baselineShift = 0;
384         }
385 
386         mText = text;
387         mPaint = paint;
388         mWidth = width;
389         mAlignment = align;
390         mSpacingMult = spacingMult;
391         mSpacingAdd = spacingAdd;
392         mSpannedText = text instanceof Spanned;
393         mTextDir = textDir;
394         mIncludePad = includePad;
395         mFallbackLineSpacing = fallbackLineSpacing;
396         mEllipsizedWidth = ellipsize == null ? width : ellipsizedWidth;
397         mEllipsize = ellipsize;
398         mMaxLines = maxLines;
399         mBreakStrategy = breakStrategy;
400         mHyphenationFrequency = hyphenationFrequency;
401         mLeftIndents = leftIndents;
402         mRightIndents = rightIndents;
403         mJustificationMode = justificationMode;
404         mLineBreakConfig = lineBreakConfig;
405         mUseBoundsForWidth = useBoundsForWidth;
406         mShiftDrawingOffsetForStartOverhang = shiftDrawingOffsetForStartOverhang;
407         mMinimumFontMetrics = minimumFontMetrics;
408 
409         initSpanColors();
410     }
411 
initSpanColors()412     private void initSpanColors() {
413         if (mSpannedText && Flags.highContrastTextSmallTextRect()) {
414             if (mSpanColors == null) {
415                 mSpanColors = new SpanColors();
416             } else {
417                 mSpanColors.recycle();
418             }
419         } else {
420             mSpanColors = null;
421         }
422     }
423 
424     /**
425      * Replace constructor properties of this Layout with new ones.  Be careful.
426      */
replaceWith(CharSequence text, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd)427     /* package */ void replaceWith(CharSequence text, TextPaint paint,
428                               int width, Alignment align,
429                               float spacingmult, float spacingadd) {
430         if (width < 0) {
431             throw new IllegalArgumentException("Layout: " + width + " < 0");
432         }
433 
434         mText = text;
435         mPaint = paint;
436         mWidth = width;
437         mAlignment = align;
438         mSpacingMult = spacingmult;
439         mSpacingAdd = spacingadd;
440         mSpannedText = text instanceof Spanned;
441         initSpanColors();
442     }
443 
444     /**
445      * Draw this Layout on the specified Canvas.
446      *
447      * This API draws background first, then draws text on top of it.
448      *
449      * @see #draw(Canvas, List, List, Path, Paint, int)
450      */
draw(Canvas c)451     public void draw(Canvas c) {
452         draw(c, (Path) null, (Paint) null, 0);
453     }
454 
455     /**
456      * Draw this Layout on the specified canvas, with the highlight path drawn
457      * between the background and the text.
458      *
459      * @param canvas the canvas
460      * @param selectionHighlight the path of the selection highlight or cursor; can be null
461      * @param selectionHighlightPaint the paint for the selection highlight
462      * @param cursorOffsetVertical the amount to temporarily translate the
463      *        canvas while rendering the highlight
464      *
465      * @see #draw(Canvas, List, List, Path, Paint, int)
466      */
draw( Canvas canvas, Path selectionHighlight, Paint selectionHighlightPaint, int cursorOffsetVertical)467     public void draw(
468             Canvas canvas, Path selectionHighlight,
469             Paint selectionHighlightPaint, int cursorOffsetVertical) {
470         draw(canvas, null, null, selectionHighlight, selectionHighlightPaint, cursorOffsetVertical);
471     }
472 
473     /**
474      * Draw this layout on the specified canvas.
475      *
476      * This API draws background first, then draws highlight paths on top of it, then draws
477      * selection or cursor, then finally draws text on top of it.
478      *
479      * @see #drawBackground(Canvas)
480      * @see #drawText(Canvas)
481      *
482      * @param canvas the canvas
483      * @param highlightPaths the path of the highlights. The highlightPaths and highlightPaints must
484      *                      have the same length and aligned in the same order. For example, the
485      *                      paint of the n-th of the highlightPaths should be stored at the n-th of
486      *                      highlightPaints.
487      * @param highlightPaints the paints for the highlights. The highlightPaths and highlightPaints
488      *                        must have the same length and aligned in the same order. For example,
489      *                        the paint of the n-th of the highlightPaths should be stored at the
490      *                        n-th of highlightPaints.
491      * @param selectionPath the selection or cursor path
492      * @param selectionPaint the paint for the selection or cursor.
493      * @param cursorOffsetVertical the amount to temporarily translate the canvas while rendering
494      *                            the highlight
495      */
draw(@onNull Canvas canvas, @Nullable List<Path> highlightPaths, @Nullable List<Paint> highlightPaints, @Nullable Path selectionPath, @Nullable Paint selectionPaint, int cursorOffsetVertical)496     public void draw(@NonNull Canvas canvas,
497             @Nullable List<Path> highlightPaths,
498             @Nullable List<Paint> highlightPaints,
499             @Nullable Path selectionPath,
500             @Nullable Paint selectionPaint,
501             int cursorOffsetVertical) {
502         float leftShift = 0;
503         if (mUseBoundsForWidth && mShiftDrawingOffsetForStartOverhang) {
504             RectF drawingRect = computeDrawingBoundingBox();
505             if (drawingRect.left < 0) {
506                 leftShift = -drawingRect.left;
507                 canvas.translate(leftShift, 0);
508             }
509         }
510         final long lineRange = getLineRangeForDraw(canvas);
511         int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
512         int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
513         if (lastLine < 0) return;
514 
515         if (shouldDrawHighlightsOnTop(canvas)) {
516             drawBackground(canvas, firstLine, lastLine);
517         } else {
518             drawWithoutText(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint,
519                     cursorOffsetVertical, firstLine, lastLine);
520         }
521 
522         drawText(canvas, firstLine, lastLine);
523 
524         // Since high contrast text draws a thick border on the text, the highlight actually makes
525         // it harder to read. In this case we draw over the top of the text with a blend mode that
526         // ensures the text stays high-contrast.
527         if (shouldDrawHighlightsOnTop(canvas)) {
528             drawHighlights(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint,
529                     cursorOffsetVertical, firstLine, lastLine);
530         }
531 
532         if (leftShift != 0) {
533             // Manually translate back to the original position because of b/324498002, using
534             // save/restore disappears the toggle switch drawables.
535             canvas.translate(-leftShift, 0);
536         }
537     }
538 
shouldDrawHighlightsOnTop(Canvas canvas)539     private static boolean shouldDrawHighlightsOnTop(Canvas canvas) {
540         return Flags.highContrastTextSmallTextRect() && canvas.isHighContrastTextEnabled();
541     }
542 
setToHighlightPaint(Paint p, BlendMode blendMode, Paint outPaint)543     private static Paint setToHighlightPaint(Paint p, BlendMode blendMode, Paint outPaint) {
544         if (p == null) return null;
545         outPaint.set(p);
546         outPaint.setBlendMode(blendMode);
547         // Yellow for maximum contrast
548         outPaint.setColor(Color.YELLOW);
549         return outPaint;
550     }
551 
552     /**
553      * Draw text part of this layout.
554      *
555      * Different from {@link #draw(Canvas, List, List, Path, Paint, int)} API, this API only draws
556      * text part, not drawing highlights, selections, or backgrounds.
557      *
558      * @see #draw(Canvas, List, List, Path, Paint, int)
559      * @see #drawBackground(Canvas)
560      *
561      * @param canvas the canvas
562      */
drawText(@onNull Canvas canvas)563     public void drawText(@NonNull Canvas canvas) {
564         final long lineRange = getLineRangeForDraw(canvas);
565         int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
566         int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
567         if (lastLine < 0) return;
568         drawText(canvas, firstLine, lastLine);
569     }
570 
571     /**
572      * Draw background of this layout.
573      *
574      * Different from {@link #draw(Canvas, List, List, Path, Paint, int)} API, this API only draws
575      * background, not drawing text, highlights or selections. The background here is drawn by
576      * {@link LineBackgroundSpan} attached to the text.
577      *
578      * @see #draw(Canvas, List, List, Path, Paint, int)
579      * @see #drawText(Canvas)
580      *
581      * @param canvas the canvas
582      */
drawBackground(@onNull Canvas canvas)583     public void drawBackground(@NonNull Canvas canvas) {
584         final long lineRange = getLineRangeForDraw(canvas);
585         int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
586         int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
587         if (lastLine < 0) return;
588         drawBackground(canvas, firstLine, lastLine);
589     }
590 
591     /**
592      * @hide public for Editor.java
593      */
drawWithoutText( @onNull Canvas canvas, @Nullable List<Path> highlightPaths, @Nullable List<Paint> highlightPaints, @Nullable Path selectionPath, @Nullable Paint selectionPaint, int cursorOffsetVertical, int firstLine, int lastLine)594     public void drawWithoutText(
595             @NonNull Canvas canvas,
596             @Nullable List<Path> highlightPaths,
597             @Nullable List<Paint> highlightPaints,
598             @Nullable Path selectionPath,
599             @Nullable Paint selectionPaint,
600             int cursorOffsetVertical,
601             int firstLine,
602             int lastLine) {
603         drawBackground(canvas, firstLine, lastLine);
604         drawHighlights(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint,
605                 cursorOffsetVertical, firstLine, lastLine);
606     }
607 
608     /**
609      * @hide public for Editor.java
610      */
drawHighlights( @onNull Canvas canvas, @Nullable List<Path> highlightPaths, @Nullable List<Paint> highlightPaints, @Nullable Path selectionPath, @Nullable Paint selectionPaint, int cursorOffsetVertical, int firstLine, int lastLine)611     public void drawHighlights(
612             @NonNull Canvas canvas,
613             @Nullable List<Path> highlightPaths,
614             @Nullable List<Paint> highlightPaints,
615             @Nullable Path selectionPath,
616             @Nullable Paint selectionPaint,
617             int cursorOffsetVertical,
618             int firstLine,
619             int lastLine) {
620         if (highlightPaths == null && highlightPaints == null) {
621             return;
622         }
623         if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical);
624         try {
625             BlendMode blendMode = determineHighContrastHighlightBlendMode(canvas);
626             if (highlightPaths != null) {
627                 if (highlightPaints == null) {
628                     throw new IllegalArgumentException(
629                             "if highlight is specified, highlightPaint must be specified.");
630                 }
631                 if (highlightPaints.size() != highlightPaths.size()) {
632                     throw new IllegalArgumentException(
633                             "The highlight path size is different from the size of highlight"
634                                     + " paints");
635                 }
636                 for (int i = 0; i < highlightPaths.size(); ++i) {
637                     final Path highlight = highlightPaths.get(i);
638                     Paint highlightPaint = highlightPaints.get(i);
639                     if (shouldDrawHighlightsOnTop(canvas)) {
640                         highlightPaint = setToHighlightPaint(highlightPaint, blendMode,
641                                 mWorkPlainPaint);
642                     }
643 
644                     if (highlight != null) {
645                         canvas.drawPath(highlight, highlightPaint);
646                     }
647                 }
648             }
649 
650             if (selectionPath != null) {
651                 if (shouldDrawHighlightsOnTop(canvas)) {
652                     selectionPaint = setToHighlightPaint(selectionPaint, blendMode,
653                             mWorkPlainPaint);
654                 }
655                 canvas.drawPath(selectionPath, selectionPaint);
656             }
657         } finally {
658             if (cursorOffsetVertical != 0) canvas.translate(0, -cursorOffsetVertical);
659         }
660     }
661 
662     @Nullable
determineHighContrastHighlightBlendMode(Canvas canvas)663     private BlendMode determineHighContrastHighlightBlendMode(Canvas canvas) {
664         if (!shouldDrawHighlightsOnTop(canvas)) {
665             return null;
666         }
667 
668         return isHighContrastTextDark(mPaint.getColor()) ? BlendMode.MULTIPLY
669                 : BlendMode.DIFFERENCE;
670     }
671 
isHighContrastTextDark(@olorInt int color)672     private boolean isHighContrastTextDark(@ColorInt int color) {
673         // High-contrast text mode
674         // Determine if the text is black-on-white or white-on-black, so we know what blendmode will
675         // give the highest contrast and most realistic text color.
676         // LINT.IfChange(hct_darken)
677         var lab = new double[3];
678         ColorUtils.colorToLAB(color, lab);
679         return lab[0] <= 50.0;
680         // LINT.ThenChange(/libs/hwui/hwui/DrawTextFunctor.h:hct_darken)
681     }
682 
isJustificationRequired(int lineNum)683     private boolean isJustificationRequired(int lineNum) {
684         if (mJustificationMode == JUSTIFICATION_MODE_NONE) return false;
685         final int lineEnd = getLineEnd(lineNum);
686         return lineEnd < mText.length() && mText.charAt(lineEnd - 1) != '\n';
687     }
688 
getJustifyWidth(int lineNum)689     private float getJustifyWidth(int lineNum) {
690         Alignment paraAlign = mAlignment;
691 
692         int left = 0;
693         int right = mWidth;
694 
695         final int dir = getParagraphDirection(lineNum);
696 
697         ParagraphStyle[] spans = NO_PARA_SPANS;
698         if (mSpannedText) {
699             Spanned sp = (Spanned) mText;
700             final int start = getLineStart(lineNum);
701 
702             final boolean isFirstParaLine = (start == 0 || mText.charAt(start - 1) == '\n');
703 
704             if (isFirstParaLine) {
705                 final int spanEnd = sp.nextSpanTransition(start, mText.length(),
706                         ParagraphStyle.class);
707                 spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);
708 
709                 for (int n = spans.length - 1; n >= 0; n--) {
710                     if (spans[n] instanceof AlignmentSpan) {
711                         paraAlign = ((AlignmentSpan) spans[n]).getAlignment();
712                         break;
713                     }
714                 }
715             }
716 
717             final int length = spans.length;
718             boolean useFirstLineMargin = isFirstParaLine;
719             for (int n = 0; n < length; n++) {
720                 if (spans[n] instanceof LeadingMarginSpan2) {
721                     int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount();
722                     int startLine = getLineForOffset(sp.getSpanStart(spans[n]));
723                     if (lineNum < startLine + count) {
724                         useFirstLineMargin = true;
725                         break;
726                     }
727                 }
728             }
729             for (int n = 0; n < length; n++) {
730                 if (spans[n] instanceof LeadingMarginSpan) {
731                     LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
732                     if (dir == DIR_RIGHT_TO_LEFT) {
733                         right -= margin.getLeadingMargin(useFirstLineMargin);
734                     } else {
735                         left += margin.getLeadingMargin(useFirstLineMargin);
736                     }
737                 }
738             }
739         }
740 
741         final Alignment align;
742         if (paraAlign == Alignment.ALIGN_LEFT) {
743             align = (dir == DIR_LEFT_TO_RIGHT) ?  Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
744         } else if (paraAlign == Alignment.ALIGN_RIGHT) {
745             align = (dir == DIR_LEFT_TO_RIGHT) ?  Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
746         } else {
747             align = paraAlign;
748         }
749 
750         final int indentWidth;
751         if (align == Alignment.ALIGN_NORMAL) {
752             if (dir == DIR_LEFT_TO_RIGHT) {
753                 indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
754             } else {
755                 indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
756             }
757         } else if (align == Alignment.ALIGN_OPPOSITE) {
758             if (dir == DIR_LEFT_TO_RIGHT) {
759                 indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
760             } else {
761                 indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
762             }
763         } else { // Alignment.ALIGN_CENTER
764             indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
765         }
766 
767         return right - left - indentWidth;
768     }
769 
770     /**
771      * @hide
772      */
773     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
drawText(Canvas canvas, int firstLine, int lastLine)774     public void drawText(Canvas canvas, int firstLine, int lastLine) {
775         int previousLineBottom = getLineTop(firstLine);
776         int previousLineEnd = getLineStart(firstLine);
777         ParagraphStyle[] spans = NO_PARA_SPANS;
778         int spanEnd = 0;
779         final TextPaint paint = mWorkPaint;
780         paint.set(mPaint);
781         CharSequence buf = mText;
782 
783         Alignment paraAlign = mAlignment;
784         TabStops tabStops = null;
785         boolean tabStopsIsInitialized = false;
786 
787         TextLine tl = TextLine.obtain();
788 
789         // Draw the lines, one at a time.
790         // The baseline is the top of the following line minus the current line's descent.
791         for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {
792             int start = previousLineEnd;
793             previousLineEnd = getLineStart(lineNum + 1);
794             final boolean justify = isJustificationRequired(lineNum);
795             int end = getLineVisibleEnd(lineNum, start, previousLineEnd,
796                     true /* trailingSpaceAtLastLineIsVisible */);
797             paint.setStartHyphenEdit(getStartHyphenEdit(lineNum));
798             paint.setEndHyphenEdit(getEndHyphenEdit(lineNum));
799 
800             int ltop = previousLineBottom;
801             int lbottom = getLineTop(lineNum + 1);
802             previousLineBottom = lbottom;
803             int lbaseline = lbottom - getLineDescent(lineNum);
804 
805             int dir = getParagraphDirection(lineNum);
806             int left = 0;
807             int right = mWidth;
808 
809             if (mSpannedText) {
810                 Spanned sp = (Spanned) buf;
811                 int textLength = buf.length();
812                 boolean isFirstParaLine = (start == 0 || buf.charAt(start - 1) == '\n');
813 
814                 // New batch of paragraph styles, collect into spans array.
815                 // Compute the alignment, last alignment style wins.
816                 // Reset tabStops, we'll rebuild if we encounter a line with
817                 // tabs.
818                 // We expect paragraph spans to be relatively infrequent, use
819                 // spanEnd so that we can check less frequently.  Since
820                 // paragraph styles ought to apply to entire paragraphs, we can
821                 // just collect the ones present at the start of the paragraph.
822                 // If spanEnd is before the end of the paragraph, that's not
823                 // our problem.
824                 if (start >= spanEnd && (lineNum == firstLine || isFirstParaLine)) {
825                     spanEnd = sp.nextSpanTransition(start, textLength,
826                                                     ParagraphStyle.class);
827                     spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);
828 
829                     paraAlign = mAlignment;
830                     for (int n = spans.length - 1; n >= 0; n--) {
831                         if (spans[n] instanceof AlignmentSpan) {
832                             paraAlign = ((AlignmentSpan) spans[n]).getAlignment();
833                             break;
834                         }
835                     }
836 
837                     tabStopsIsInitialized = false;
838                 }
839 
840                 // Draw all leading margin spans.  Adjust left or right according
841                 // to the paragraph direction of the line.
842                 final int length = spans.length;
843                 boolean useFirstLineMargin = isFirstParaLine;
844                 for (int n = 0; n < length; n++) {
845                     if (spans[n] instanceof LeadingMarginSpan2) {
846                         int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount();
847                         int startLine = getLineForOffset(sp.getSpanStart(spans[n]));
848                         // if there is more than one LeadingMarginSpan2, use
849                         // the count that is greatest
850                         if (lineNum < startLine + count) {
851                             useFirstLineMargin = true;
852                             break;
853                         }
854                     }
855                 }
856                 for (int n = 0; n < length; n++) {
857                     if (spans[n] instanceof LeadingMarginSpan) {
858                         LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
859                         if (dir == DIR_RIGHT_TO_LEFT) {
860                             margin.drawLeadingMargin(canvas, paint, right, dir, ltop,
861                                                      lbaseline, lbottom, buf,
862                                                      start, end, isFirstParaLine, this);
863                             right -= margin.getLeadingMargin(useFirstLineMargin);
864                         } else {
865                             margin.drawLeadingMargin(canvas, paint, left, dir, ltop,
866                                                      lbaseline, lbottom, buf,
867                                                      start, end, isFirstParaLine, this);
868                             left += margin.getLeadingMargin(useFirstLineMargin);
869                         }
870                     }
871                 }
872             }
873 
874             boolean hasTab = getLineContainsTab(lineNum);
875             // Can't tell if we have tabs for sure, currently
876             if (hasTab && !tabStopsIsInitialized) {
877                 if (tabStops == null) {
878                     tabStops = new TabStops(TAB_INCREMENT, spans);
879                 } else {
880                     tabStops.reset(TAB_INCREMENT, spans);
881                 }
882                 tabStopsIsInitialized = true;
883             }
884 
885             // Determine whether the line aligns to normal, opposite, or center.
886             Alignment align = paraAlign;
887             if (align == Alignment.ALIGN_LEFT) {
888                 align = (dir == DIR_LEFT_TO_RIGHT) ?
889                     Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
890             } else if (align == Alignment.ALIGN_RIGHT) {
891                 align = (dir == DIR_LEFT_TO_RIGHT) ?
892                     Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
893             }
894 
895             int x;
896             final int indentWidth;
897             if (align == Alignment.ALIGN_NORMAL) {
898                 if (dir == DIR_LEFT_TO_RIGHT) {
899                     indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
900                     x = left + indentWidth;
901                 } else {
902                     indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
903                     x = right - indentWidth;
904                 }
905             } else {
906                 int max = (int)getLineExtent(lineNum, tabStops, false);
907                 if (align == Alignment.ALIGN_OPPOSITE) {
908                     if (dir == DIR_LEFT_TO_RIGHT) {
909                         indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
910                         x = right - max - indentWidth;
911                     } else {
912                         indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
913                         x = left - max + indentWidth;
914                     }
915                 } else { // Alignment.ALIGN_CENTER
916                     indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
917                     max = max & ~1;
918                     x = ((right + left - max) >> 1) + indentWidth;
919                 }
920             }
921 
922             Directions directions = getLineDirections(lineNum);
923             if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab && !justify) {
924                 // XXX: assumes there's nothing additional to be done
925                 canvas.drawText(buf, start, end, x, lbaseline, paint);
926             } else {
927                 tl.set(paint, buf, start, end, dir, directions, hasTab, tabStops,
928                         getEllipsisStart(lineNum),
929                         getEllipsisStart(lineNum) + getEllipsisCount(lineNum),
930                         isFallbackLineSpacingEnabled());
931                 if (justify) {
932                     tl.justify(mJustificationMode, right - left - indentWidth);
933                 }
934                 tl.draw(canvas, x, ltop, lbaseline, lbottom);
935             }
936         }
937 
938         TextLine.recycle(tl);
939     }
940 
941     /**
942      * @hide
943      */
944     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
drawBackground( @onNull Canvas canvas, int firstLine, int lastLine)945     public void drawBackground(
946             @NonNull Canvas canvas,
947             int firstLine, int lastLine) {
948 
949         drawHighContrastBackground(canvas, firstLine, lastLine);
950 
951         // First, draw LineBackgroundSpans.
952         // LineBackgroundSpans know nothing about the alignment, margins, or
953         // direction of the layout or line.  XXX: Should they?
954         // They are evaluated at each line.
955         if (mSpannedText) {
956             if (mLineBackgroundSpans == null) {
957                 mLineBackgroundSpans = new SpanSet<LineBackgroundSpan>(LineBackgroundSpan.class);
958             }
959 
960             Spanned buffer = (Spanned) mText;
961             int textLength = buffer.length();
962             mLineBackgroundSpans.init(buffer, 0, textLength);
963 
964             if (mLineBackgroundSpans.numberOfSpans > 0) {
965                 int previousLineBottom = getLineTop(firstLine);
966                 int previousLineEnd = getLineStart(firstLine);
967                 ParagraphStyle[] spans = NO_PARA_SPANS;
968                 int spansLength = 0;
969                 TextPaint paint = mPaint;
970                 int spanEnd = 0;
971                 final int width = mWidth;
972                 for (int i = firstLine; i <= lastLine; i++) {
973                     int start = previousLineEnd;
974                     int end = getLineStart(i + 1);
975                     previousLineEnd = end;
976 
977                     int ltop = previousLineBottom;
978                     int lbottom = getLineTop(i + 1);
979                     previousLineBottom = lbottom;
980                     int lbaseline = lbottom - getLineDescent(i);
981 
982                     if (end >= spanEnd) {
983                         // These should be infrequent, so we'll use this so that
984                         // we don't have to check as often.
985                         spanEnd = mLineBackgroundSpans.getNextTransition(start, textLength);
986                         // All LineBackgroundSpans on a line contribute to its background.
987                         spansLength = 0;
988                         // Duplication of the logic of getParagraphSpans
989                         if (start != end || start == 0) {
990                             // Equivalent to a getSpans(start, end), but filling the 'spans' local
991                             // array instead to reduce memory allocation
992                             for (int j = 0; j < mLineBackgroundSpans.numberOfSpans; j++) {
993                                 // equal test is valid since both intervals are not empty by
994                                 // construction
995                                 if (mLineBackgroundSpans.spanStarts[j] >= end ||
996                                         mLineBackgroundSpans.spanEnds[j] <= start) continue;
997                                 spans = GrowingArrayUtils.append(
998                                         spans, spansLength, mLineBackgroundSpans.spans[j]);
999                                 spansLength++;
1000                             }
1001                         }
1002                     }
1003 
1004                     for (int n = 0; n < spansLength; n++) {
1005                         LineBackgroundSpan lineBackgroundSpan = (LineBackgroundSpan) spans[n];
1006                         lineBackgroundSpan.drawBackground(canvas, paint, 0, width,
1007                                 ltop, lbaseline, lbottom,
1008                                 buffer, start, end, i);
1009                     }
1010                 }
1011             }
1012             mLineBackgroundSpans.recycle();
1013         }
1014     }
1015 
1016     /**
1017      * Draws a solid rectangle behind the text, the same color as the high contrast stroke border,
1018      * to make it even easier to read.
1019      *
1020      * <p>We draw it here instead of in DrawTextFunctor so that multiple spans don't draw
1021      * backgrounds over each other's text.
1022      */
drawHighContrastBackground(@onNull Canvas canvas, int firstLine, int lastLine)1023     private void drawHighContrastBackground(@NonNull Canvas canvas, int firstLine, int lastLine) {
1024         if (!shouldDrawHighlightsOnTop(canvas)) {
1025             return;
1026         }
1027 
1028         if (!mSpannedText || mSpanColors == null) {
1029             if (mPaint.getAlpha() == 0) {
1030                 return;
1031             }
1032         }
1033 
1034         var padding = Math.max(HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX,
1035                 mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR);
1036         var cornerRadius = Math.max(
1037                 mPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP,
1038                 mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR);
1039 
1040         // We set the alpha on the color itself instead of Paint.setAlpha(), because that function
1041         // actually mutates the color in... *ehem* very strange ways. Also the color might get reset
1042         // for various reasons, which also resets the alpha.
1043         var white = Color.argb(HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE, 1f, 1f, 1f);
1044         var black = Color.argb(HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE, 0f, 0f, 0f);
1045 
1046         var originalTextColor = mPaint.getColor();
1047         var bgPaint = mWorkPlainPaint;
1048         bgPaint.reset();
1049         bgPaint.setColor(isHighContrastTextDark(originalTextColor) ? white : black);
1050         bgPaint.setStyle(Paint.Style.FILL);
1051 
1052         int start = getLineStart(firstLine);
1053         int end = getLineEnd(lastLine);
1054         // Draw a separate background rectangle for each line of text, that only surrounds the
1055         // characters on that line. But we also have to check the text color for each character, and
1056         // make sure we are drawing the correct contrasting background. This is because Spans can
1057         // change colors throughout the text and we'll need to match our backgrounds.
1058         if (mSpannedText && mSpanColors != null) {
1059             mSpanColors.init(mWorkPaint, ((Spanned) mText), start, end);
1060         }
1061 
1062         forEachCharacterBounds(
1063                 start,
1064                 end,
1065                 firstLine,
1066                 lastLine,
1067                 new CharacterBoundsListener() {
1068                     int mLastLineNum = -1;
1069                     final RectF mLineBackground = new RectF();
1070 
1071                     @ColorInt int mLastColor = originalTextColor;
1072 
1073                     @Override
1074                     public void onCharacterBounds(int index, int lineNum, float left, float top,
1075                             float right, float bottom) {
1076 
1077                         // Skip processing if the character is a space or a tap to avoid
1078                         // rendering an abrupt, empty rectangle.
1079                         if (TextLine.isLineEndSpace(mText.charAt(index))) {
1080                             return;
1081                         }
1082 
1083                         var newBackground = determineContrastingBackgroundColor(index);
1084                         var hasBgColorChanged = newBackground != bgPaint.getColor();
1085 
1086                         // To avoid highlighting emoji sequences, we use Extended_Pictgraphs as a
1087                         // heuristic. Highlighting is skipped based on code points, not glyph type
1088                         // (text vs. color), so emojis with default text presentation are
1089                         // intentionally not highlighted (numeric representation with emoji
1090                         // presentation are manually excluded). Although we process ZWJ and
1091                         // variation selectors within emoji sequences, they should not affect
1092                         // highlighting due to their zero-width nature.
1093                         var codePoint = Character.codePointAt(mText, index);
1094                         var isEmoji = Character.isEmojiComponent(codePoint)
1095                                 || Character.isExtendedPictographic(codePoint);
1096                         if (isEmoji && !isStandardNumber(index)) {
1097                             return;
1098                         }
1099 
1100                         if (lineNum != mLastLineNum || hasBgColorChanged) {
1101                             // Draw what we have so far, then reset the rect and update its color
1102                             drawRect();
1103                             mLineBackground.set(left, top, right, bottom);
1104                             mLastLineNum = lineNum;
1105 
1106                             if (hasBgColorChanged) {
1107                                 bgPaint.setColor(newBackground);
1108                             }
1109                         } else {
1110                             mLineBackground.union(left, top, right, bottom);
1111                         }
1112                     }
1113 
1114                     @Override
1115                     public void onEnd() {
1116                         drawRect();
1117                     }
1118 
1119                     private boolean isStandardNumber(int index) {
1120                         var codePoint = Character.codePointAt(mText, index);
1121                         var isNumberSignOrAsterisk = (codePoint >= '0' && codePoint <= '9')
1122                                 || codePoint == '#' || codePoint == '*';
1123                         var isColoredGlyph = index + 1 < mText.length()
1124                                 && Character.codePointAt(mText, index + 1) == 0xFE0F;
1125 
1126                         return isNumberSignOrAsterisk && !isColoredGlyph;
1127                     }
1128 
1129                     private void drawRect() {
1130                         if (!mLineBackground.isEmpty()) {
1131                             mLineBackground.inset(-padding, -padding);
1132                             canvas.drawRoundRect(
1133                                     mLineBackground,
1134                                     cornerRadius,
1135                                     cornerRadius,
1136                                     bgPaint
1137                             );
1138                         }
1139                     }
1140 
1141                     private int determineContrastingBackgroundColor(int index) {
1142                         if (!mSpannedText || mSpanColors == null) {
1143                             // The text is not Spanned. it's all one color.
1144                             return bgPaint.getColor();
1145                         }
1146 
1147                         // Sometimes the color will change, but not enough to warrant a background
1148                         // color change. e.g. from black to dark grey still gets clamped to black,
1149                         // so the background stays white and we don't need to draw a fresh
1150                         // background.
1151                         var textColor = mSpanColors.getColorAt(index);
1152                         if (textColor == SpanColors.NO_COLOR_FOUND) {
1153                             textColor = originalTextColor;
1154                         }
1155                         var hasColorChanged = textColor != mLastColor;
1156                         if (hasColorChanged) {
1157                             mLastColor = textColor;
1158 
1159                             return isHighContrastTextDark(textColor) ? white : black;
1160                         }
1161 
1162                         return bgPaint.getColor();
1163                     }
1164                 }
1165         );
1166 
1167         if (mSpanColors != null) {
1168             mSpanColors.recycle();
1169         }
1170     }
1171 
1172     /**
1173      * @param canvas
1174      * @return The range of lines that need to be drawn, possibly empty.
1175      * @hide
1176      */
1177     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getLineRangeForDraw(Canvas canvas)1178     public long getLineRangeForDraw(Canvas canvas) {
1179         int dtop, dbottom;
1180 
1181         synchronized (sTempRect) {
1182             if (!canvas.getClipBounds(sTempRect)) {
1183                 // Negative range end used as a special flag
1184                 return TextUtils.packRangeInLong(0, -1);
1185             }
1186 
1187             dtop = sTempRect.top;
1188             dbottom = sTempRect.bottom;
1189         }
1190 
1191         final int top = Math.max(dtop, 0);
1192         final int bottom = Math.min(getLineTop(getLineCount()), dbottom);
1193 
1194         if (top >= bottom) return TextUtils.packRangeInLong(0, -1);
1195         return TextUtils.packRangeInLong(getLineForVertical(top), getLineForVertical(bottom));
1196     }
1197 
1198     /**
1199      * Return the start position of the line, given the left and right bounds of the margins.
1200      *
1201      * @param line the line index
1202      * @param left the left bounds (0, or leading margin if ltr para)
1203      * @param right the right bounds (width, minus leading margin if rtl para)
1204      * @return the start position of the line (to right of line if rtl para)
1205      */
getLineStartPos(int line, int left, int right)1206     private int getLineStartPos(int line, int left, int right) {
1207         // Adjust the point at which to start rendering depending on the
1208         // alignment of the paragraph.
1209         Alignment align = getParagraphAlignment(line);
1210         int dir = getParagraphDirection(line);
1211 
1212         if (align == Alignment.ALIGN_LEFT) {
1213             align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
1214         } else if (align == Alignment.ALIGN_RIGHT) {
1215             align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
1216         }
1217 
1218         int x;
1219         if (align == Alignment.ALIGN_NORMAL) {
1220             if (dir == DIR_LEFT_TO_RIGHT) {
1221                 x = left + getIndentAdjust(line, Alignment.ALIGN_LEFT);
1222             } else {
1223                 x = right + getIndentAdjust(line, Alignment.ALIGN_RIGHT);
1224             }
1225         } else {
1226             TabStops tabStops = null;
1227             if (mSpannedText && getLineContainsTab(line)) {
1228                 Spanned spanned = (Spanned) mText;
1229                 int start = getLineStart(line);
1230                 int spanEnd = spanned.nextSpanTransition(start, spanned.length(),
1231                         TabStopSpan.class);
1232                 TabStopSpan[] tabSpans = getParagraphSpans(spanned, start, spanEnd,
1233                         TabStopSpan.class);
1234                 if (tabSpans.length > 0) {
1235                     tabStops = new TabStops(TAB_INCREMENT, tabSpans);
1236                 }
1237             }
1238             int max = (int)getLineExtent(line, tabStops, false);
1239             if (align == Alignment.ALIGN_OPPOSITE) {
1240                 if (dir == DIR_LEFT_TO_RIGHT) {
1241                     x = right - max + getIndentAdjust(line, Alignment.ALIGN_RIGHT);
1242                 } else {
1243                     // max is negative here
1244                     x = left - max + getIndentAdjust(line, Alignment.ALIGN_LEFT);
1245                 }
1246             } else { // Alignment.ALIGN_CENTER
1247                 max = max & ~1;
1248                 x = (left + right - max) >> 1 + getIndentAdjust(line, Alignment.ALIGN_CENTER);
1249             }
1250         }
1251         return x;
1252     }
1253 
1254     /**
1255      * Increase the width of this layout to the specified width.
1256      * Be careful to use this only when you know it is appropriate&mdash;
1257      * it does not cause the text to reflow to use the full new width.
1258      */
increaseWidthTo(int wid)1259     public final void increaseWidthTo(int wid) {
1260         if (wid < mWidth) {
1261             throw new RuntimeException("attempted to reduce Layout width");
1262         }
1263 
1264         mWidth = wid;
1265     }
1266 
1267     /**
1268      * Return the total height of this layout.
1269      */
getHeight()1270     public int getHeight() {
1271         return getLineTop(getLineCount());
1272     }
1273 
1274     /**
1275      * Return the total height of this layout.
1276      *
1277      * @param cap if true and max lines is set, returns the height of the layout at the max lines.
1278      *
1279      * @hide
1280      */
getHeight(boolean cap)1281     public int getHeight(boolean cap) {
1282         return getHeight();
1283     }
1284 
1285     /**
1286      * Return the number of lines of text in this layout.
1287      */
getLineCount()1288     public abstract int getLineCount();
1289 
1290     /**
1291      * Get an actual bounding box that draws text content.
1292      *
1293      * Note that the {@link RectF#top} and {@link RectF#bottom} may be different from the
1294      * {@link Layout#getLineTop(int)} of the first line and {@link Layout#getLineBottom(int)} of
1295      * the last line. The line top and line bottom are calculated based on yMin/yMax or
1296      * ascent/descent value of font file. On the other hand, the drawing bounding boxes are
1297      * calculated based on actual glyphs used there.
1298      *
1299      * @return bounding rectangle
1300      */
1301     @NonNull
1302     @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
computeDrawingBoundingBox()1303     public RectF computeDrawingBoundingBox() {
1304         float left = 0;
1305         float right = 0;
1306         float top = 0;
1307         float bottom = 0;
1308         TextLine tl = TextLine.obtain();
1309         RectF rectF = new RectF();
1310         for (int line = 0; line < getLineCount(); ++line) {
1311             final int start = getLineStart(line);
1312             final int end = getLineVisibleEnd(line);
1313 
1314             final boolean hasTabs = getLineContainsTab(line);
1315             TabStops tabStops = null;
1316             if (hasTabs && mText instanceof Spanned) {
1317                 // Just checking this line should be good enough, tabs should be
1318                 // consistent across all lines in a paragraph.
1319                 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end,
1320                         TabStopSpan.class);
1321                 if (tabs.length > 0) {
1322                     tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
1323                 }
1324             }
1325             final Directions directions = getLineDirections(line);
1326             // Returned directions can actually be null
1327             if (directions == null) {
1328                 continue;
1329             }
1330             final int dir = getParagraphDirection(line);
1331 
1332             final TextPaint paint = mWorkPaint;
1333             paint.set(mPaint);
1334             paint.setStartHyphenEdit(getStartHyphenEdit(line));
1335             paint.setEndHyphenEdit(getEndHyphenEdit(line));
1336             tl.set(paint, mText, start, end, dir, directions, hasTabs, tabStops,
1337                     getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
1338                     isFallbackLineSpacingEnabled());
1339             if (isJustificationRequired(line)) {
1340                 tl.justify(mJustificationMode, getJustifyWidth(line));
1341             }
1342             tl.metrics(null, rectF, false, null);
1343 
1344             float lineLeft = rectF.left;
1345             float lineRight = rectF.right;
1346             float lineTop = rectF.top + getLineBaseline(line);
1347             float lineBottom = rectF.bottom + getLineBaseline(line);
1348             if (getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT) {
1349                 lineLeft += getWidth();
1350                 lineRight += getWidth();
1351             }
1352 
1353             if (line == 0) {
1354                 left = lineLeft;
1355                 right = lineRight;
1356                 top = lineTop;
1357                 bottom = lineBottom;
1358             } else {
1359                 left = Math.min(left, lineLeft);
1360                 right = Math.max(right, lineRight);
1361                 top = Math.min(top, lineTop);
1362                 bottom = Math.max(bottom, lineBottom);
1363             }
1364         }
1365         TextLine.recycle(tl);
1366         return new RectF(left, top, right, bottom);
1367     }
1368 
1369     /**
1370      * Return the baseline for the specified line (0&hellip;getLineCount() - 1)
1371      * If bounds is not null, return the top, left, right, bottom extents
1372      * of the specified line in it.
1373      * @param line which line to examine (0..getLineCount() - 1)
1374      * @param bounds Optional. If not null, it returns the extent of the line
1375      * @return the Y-coordinate of the baseline
1376      */
getLineBounds(int line, Rect bounds)1377     public int getLineBounds(int line, Rect bounds) {
1378         if (bounds != null) {
1379             bounds.left = 0;     // ???
1380             bounds.top = getLineTop(line);
1381             bounds.right = mWidth;   // ???
1382             bounds.bottom = getLineTop(line + 1);
1383         }
1384         return getLineBaseline(line);
1385     }
1386 
1387     /**
1388      * Return the vertical position of the top of the specified line
1389      * (0&hellip;getLineCount()).
1390      * If the specified line is equal to the line count, returns the
1391      * bottom of the last line.
1392      */
getLineTop(int line)1393     public abstract int getLineTop(int line);
1394 
1395     /**
1396      * Return the descent of the specified line(0&hellip;getLineCount() - 1).
1397      */
getLineDescent(int line)1398     public abstract int getLineDescent(int line);
1399 
1400     /**
1401      * Return the text offset of the beginning of the specified line (
1402      * 0&hellip;getLineCount()). If the specified line is equal to the line
1403      * count, returns the length of the text.
1404      */
getLineStart(int line)1405     public abstract int getLineStart(int line);
1406 
1407     /**
1408      * Returns the primary directionality of the paragraph containing the
1409      * specified line, either 1 for left-to-right lines, or -1 for right-to-left
1410      * lines (see {@link #DIR_LEFT_TO_RIGHT}, {@link #DIR_RIGHT_TO_LEFT}).
1411      */
getParagraphDirection(int line)1412     public abstract int getParagraphDirection(int line);
1413 
1414     /**
1415      * Returns whether the specified line contains one or more
1416      * characters that need to be handled specially, like tabs.
1417      */
getLineContainsTab(int line)1418     public abstract boolean getLineContainsTab(int line);
1419 
1420     /**
1421      * Returns the directional run information for the specified line.
1422      * The array alternates counts of characters in left-to-right
1423      * and right-to-left segments of the line.
1424      *
1425      * <p>NOTE: this is inadequate to support bidirectional text, and will change.
1426      */
getLineDirections(int line)1427     public abstract Directions getLineDirections(int line);
1428 
1429     /**
1430      * Returns the (negative) number of extra pixels of ascent padding in the
1431      * top line of the Layout.
1432      */
getTopPadding()1433     public abstract int getTopPadding();
1434 
1435     /**
1436      * Returns the number of extra pixels of descent padding in the
1437      * bottom line of the Layout.
1438      */
getBottomPadding()1439     public abstract int getBottomPadding();
1440 
1441     /**
1442      * Returns the start hyphen edit for a line.
1443      *
1444      * @hide
1445      */
getStartHyphenEdit(int line)1446     public @Paint.StartHyphenEdit int getStartHyphenEdit(int line) {
1447         return Paint.START_HYPHEN_EDIT_NO_EDIT;
1448     }
1449 
1450     /**
1451      * Returns the end hyphen edit for a line.
1452      *
1453      * @hide
1454      */
getEndHyphenEdit(int line)1455     public @Paint.EndHyphenEdit int getEndHyphenEdit(int line) {
1456         return Paint.END_HYPHEN_EDIT_NO_EDIT;
1457     }
1458 
1459     /**
1460      * Returns the left indent for a line.
1461      *
1462      * @hide
1463      */
getIndentAdjust(int line, Alignment alignment)1464     public int getIndentAdjust(int line, Alignment alignment) {
1465         return 0;
1466     }
1467 
1468     /**
1469      * Returns true if the character at offset and the preceding character
1470      * are at different run levels (and thus there's a split caret).
1471      * @param offset the offset
1472      * @return true if at a level boundary
1473      * @hide
1474      */
1475     @UnsupportedAppUsage
isLevelBoundary(int offset)1476     public boolean isLevelBoundary(int offset) {
1477         int line = getLineForOffset(offset);
1478         Directions dirs = getLineDirections(line);
1479         if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) {
1480             return false;
1481         }
1482 
1483         int[] runs = dirs.mDirections;
1484         int lineStart = getLineStart(line);
1485         int lineEnd = getLineEnd(line);
1486         if (offset == lineStart || offset == lineEnd) {
1487             int paraLevel = getParagraphDirection(line) == 1 ? 0 : 1;
1488             int runIndex = offset == lineStart ? 0 : runs.length - 2;
1489             return ((runs[runIndex + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK) != paraLevel;
1490         }
1491 
1492         offset -= lineStart;
1493         for (int i = 0; i < runs.length; i += 2) {
1494             if (offset == runs[i]) {
1495                 return true;
1496             }
1497         }
1498         return false;
1499     }
1500 
1501     /**
1502      * Returns true if the character at offset is right to left (RTL).
1503      * @param offset the offset
1504      * @return true if the character is RTL, false if it is LTR
1505      */
isRtlCharAt(int offset)1506     public boolean isRtlCharAt(int offset) {
1507         int line = getLineForOffset(offset);
1508         Directions dirs = getLineDirections(line);
1509         if (dirs == DIRS_ALL_LEFT_TO_RIGHT) {
1510             return false;
1511         }
1512         if (dirs == DIRS_ALL_RIGHT_TO_LEFT) {
1513             return  true;
1514         }
1515         int[] runs = dirs.mDirections;
1516         int lineStart = getLineStart(line);
1517         for (int i = 0; i < runs.length; i += 2) {
1518             int start = lineStart + runs[i];
1519             int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
1520             if (offset >= start && offset < limit) {
1521                 int level = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
1522                 return ((level & 1) != 0);
1523             }
1524         }
1525         // Should happen only if the offset is "out of bounds"
1526         return false;
1527     }
1528 
1529     /**
1530      * Returns the range of the run that the character at offset belongs to.
1531      * @param offset the offset
1532      * @return The range of the run
1533      * @hide
1534      */
getRunRange(int offset)1535     public long getRunRange(int offset) {
1536         int line = getLineForOffset(offset);
1537         Directions dirs = getLineDirections(line);
1538         if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) {
1539             return TextUtils.packRangeInLong(0, getLineEnd(line));
1540         }
1541         int[] runs = dirs.mDirections;
1542         int lineStart = getLineStart(line);
1543         for (int i = 0; i < runs.length; i += 2) {
1544             int start = lineStart + runs[i];
1545             int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
1546             if (offset >= start && offset < limit) {
1547                 return TextUtils.packRangeInLong(start, limit);
1548             }
1549         }
1550         // Should happen only if the offset is "out of bounds"
1551         return TextUtils.packRangeInLong(0, getLineEnd(line));
1552     }
1553 
1554     /**
1555      * Checks if the trailing BiDi level should be used for an offset
1556      *
1557      * This method is useful when the offset is at the BiDi level transition point and determine
1558      * which run need to be used. For example, let's think about following input: (L* denotes
1559      * Left-to-Right characters, R* denotes Right-to-Left characters.)
1560      * Input (Logical Order): L1 L2 L3 R1 R2 R3 L4 L5 L6
1561      * Input (Display Order): L1 L2 L3 R3 R2 R1 L4 L5 L6
1562      *
1563      * Then, think about selecting the range (3, 6). The offset=3 and offset=6 are ambiguous here
1564      * since they are at the BiDi transition point.  In Android, the offset is considered to be
1565      * associated with the trailing run if the BiDi level of the trailing run is higher than of the
1566      * previous run.  In this case, the BiDi level of the input text is as follows:
1567      *
1568      * Input (Logical Order): L1 L2 L3 R1 R2 R3 L4 L5 L6
1569      *              BiDi Run: [ Run 0 ][ Run 1 ][ Run 2 ]
1570      *            BiDi Level:  0  0  0  1  1  1  0  0  0
1571      *
1572      * Thus, offset = 3 is part of Run 1 and this method returns true for offset = 3, since the BiDi
1573      * level of Run 1 is higher than the level of Run 0.  Similarly, the offset = 6 is a part of Run
1574      * 1 and this method returns false for the offset = 6 since the BiDi level of Run 1 is higher
1575      * than the level of Run 2.
1576      *
1577      * @returns true if offset is at the BiDi level transition point and trailing BiDi level is
1578      *          higher than previous BiDi level. See above for the detail.
1579      * @hide
1580      */
1581     @VisibleForTesting
primaryIsTrailingPrevious(int offset)1582     public boolean primaryIsTrailingPrevious(int offset) {
1583         int line = getLineForOffset(offset);
1584         int lineStart = getLineStart(line);
1585         int lineEnd = getLineEnd(line);
1586         int[] runs = getLineDirections(line).mDirections;
1587 
1588         int levelAt = -1;
1589         for (int i = 0; i < runs.length; i += 2) {
1590             int start = lineStart + runs[i];
1591             int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
1592             if (limit > lineEnd) {
1593                 limit = lineEnd;
1594             }
1595             if (offset >= start && offset < limit) {
1596                 if (offset > start) {
1597                     // Previous character is at same level, so don't use trailing.
1598                     return false;
1599                 }
1600                 levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
1601                 break;
1602             }
1603         }
1604         if (levelAt == -1) {
1605             // Offset was limit of line.
1606             levelAt = getParagraphDirection(line) == 1 ? 0 : 1;
1607         }
1608 
1609         // At level boundary, check previous level.
1610         int levelBefore = -1;
1611         if (offset == lineStart) {
1612             levelBefore = getParagraphDirection(line) == 1 ? 0 : 1;
1613         } else {
1614             offset -= 1;
1615             for (int i = 0; i < runs.length; i += 2) {
1616                 int start = lineStart + runs[i];
1617                 int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
1618                 if (limit > lineEnd) {
1619                     limit = lineEnd;
1620                 }
1621                 if (offset >= start && offset < limit) {
1622                     levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
1623                     break;
1624                 }
1625             }
1626         }
1627 
1628         return levelBefore < levelAt;
1629     }
1630 
1631     /**
1632      * Computes in linear time the results of calling
1633      * #primaryIsTrailingPrevious for all offsets on a line.
1634      * @param line The line giving the offsets we compute the information for
1635      * @return The array of results, indexed from 0, where 0 corresponds to the line start offset
1636      * @hide
1637      */
1638     @VisibleForTesting
primaryIsTrailingPreviousAllLineOffsets(int line)1639     public boolean[] primaryIsTrailingPreviousAllLineOffsets(int line) {
1640         int lineStart = getLineStart(line);
1641         int lineEnd = getLineEnd(line);
1642         int[] runs = getLineDirections(line).mDirections;
1643 
1644         boolean[] trailing = new boolean[lineEnd - lineStart + 1];
1645 
1646         byte[] level = new byte[lineEnd - lineStart + 1];
1647         for (int i = 0; i < runs.length; i += 2) {
1648             int start = lineStart + runs[i];
1649             int limit = start + (runs[i + 1] & RUN_LENGTH_MASK);
1650             if (limit > lineEnd) {
1651                 limit = lineEnd;
1652             }
1653             if (limit == start) {
1654                 continue;
1655             }
1656             level[limit - lineStart - 1] =
1657                     (byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK);
1658         }
1659 
1660         for (int i = 0; i < runs.length; i += 2) {
1661             int start = lineStart + runs[i];
1662             byte currentLevel = (byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK);
1663             trailing[start - lineStart] = currentLevel > (start == lineStart
1664                     ? (getParagraphDirection(line) == 1 ? 0 : 1)
1665                     : level[start - lineStart - 1]);
1666         }
1667 
1668         return trailing;
1669     }
1670 
1671     /**
1672      * Get the primary horizontal position for the specified text offset.
1673      * This is the location where a new character would be inserted in
1674      * the paragraph's primary direction.
1675      */
getPrimaryHorizontal(int offset)1676     public float getPrimaryHorizontal(int offset) {
1677         return getPrimaryHorizontal(offset, false /* not clamped */);
1678     }
1679 
1680     /**
1681      * Get the primary horizontal position for the specified text offset, but
1682      * optionally clamp it so that it doesn't exceed the width of the layout.
1683      * @hide
1684      */
1685     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getPrimaryHorizontal(int offset, boolean clamped)1686     public float getPrimaryHorizontal(int offset, boolean clamped) {
1687         boolean trailing = primaryIsTrailingPrevious(offset);
1688         return getHorizontal(offset, trailing, clamped);
1689     }
1690 
1691     /**
1692      * Get the secondary horizontal position for the specified text offset.
1693      * This is the location where a new character would be inserted in
1694      * the direction other than the paragraph's primary direction.
1695      */
getSecondaryHorizontal(int offset)1696     public float getSecondaryHorizontal(int offset) {
1697         return getSecondaryHorizontal(offset, false /* not clamped */);
1698     }
1699 
1700     /**
1701      * Get the secondary horizontal position for the specified text offset, but
1702      * optionally clamp it so that it doesn't exceed the width of the layout.
1703      * @hide
1704      */
1705     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getSecondaryHorizontal(int offset, boolean clamped)1706     public float getSecondaryHorizontal(int offset, boolean clamped) {
1707         boolean trailing = primaryIsTrailingPrevious(offset);
1708         return getHorizontal(offset, !trailing, clamped);
1709     }
1710 
getHorizontal(int offset, boolean primary)1711     private float getHorizontal(int offset, boolean primary) {
1712         return primary ? getPrimaryHorizontal(offset) : getSecondaryHorizontal(offset);
1713     }
1714 
getHorizontal(int offset, boolean trailing, boolean clamped)1715     private float getHorizontal(int offset, boolean trailing, boolean clamped) {
1716         int line = getLineForOffset(offset);
1717 
1718         return getHorizontal(offset, trailing, line, clamped);
1719     }
1720 
getHorizontal(int offset, boolean trailing, int line, boolean clamped)1721     private float getHorizontal(int offset, boolean trailing, int line, boolean clamped) {
1722         int start = getLineStart(line);
1723         int end = getLineEnd(line);
1724         int dir = getParagraphDirection(line);
1725         boolean hasTab = getLineContainsTab(line);
1726         Directions directions = getLineDirections(line);
1727 
1728         TabStops tabStops = null;
1729         if (hasTab && mText instanceof Spanned) {
1730             // Just checking this line should be good enough, tabs should be
1731             // consistent across all lines in a paragraph.
1732             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
1733             if (tabs.length > 0) {
1734                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
1735             }
1736         }
1737 
1738         TextLine tl = TextLine.obtain();
1739         tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops,
1740                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
1741                 isFallbackLineSpacingEnabled());
1742         float wid = tl.measure(offset - start, trailing, null, null, null);
1743         TextLine.recycle(tl);
1744 
1745         if (clamped && wid > mWidth) {
1746             wid = mWidth;
1747         }
1748         int left = getParagraphLeft(line);
1749         int right = getParagraphRight(line);
1750 
1751         return getLineStartPos(line, left, right) + wid;
1752     }
1753 
1754     /**
1755      * Computes in linear time the results of calling #getHorizontal for all offsets on a line.
1756      *
1757      * @param line The line giving the offsets we compute information for
1758      * @param clamped Whether to clamp the results to the width of the layout
1759      * @param primary Whether the results should be the primary or the secondary horizontal
1760      * @return The array of results, indexed from 0, where 0 corresponds to the line start offset
1761      */
getLineHorizontals(int line, boolean clamped, boolean primary)1762     private float[] getLineHorizontals(int line, boolean clamped, boolean primary) {
1763         int start = getLineStart(line);
1764         int end = getLineEnd(line);
1765         int dir = getParagraphDirection(line);
1766         boolean hasTab = getLineContainsTab(line);
1767         Directions directions = getLineDirections(line);
1768 
1769         TabStops tabStops = null;
1770         if (hasTab && mText instanceof Spanned) {
1771             // Just checking this line should be good enough, tabs should be
1772             // consistent across all lines in a paragraph.
1773             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
1774             if (tabs.length > 0) {
1775                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
1776             }
1777         }
1778 
1779         TextLine tl = TextLine.obtain();
1780         tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops,
1781                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
1782                 isFallbackLineSpacingEnabled());
1783         boolean[] trailings = primaryIsTrailingPreviousAllLineOffsets(line);
1784         if (!primary) {
1785             for (int offset = 0; offset < trailings.length; ++offset) {
1786                 trailings[offset] = !trailings[offset];
1787             }
1788         }
1789         float[] wid = tl.measureAllOffsets(trailings, null);
1790         TextLine.recycle(tl);
1791 
1792         if (clamped) {
1793             for (int offset = 0; offset < wid.length; ++offset) {
1794                 if (wid[offset] > mWidth) {
1795                     wid[offset] = mWidth;
1796                 }
1797             }
1798         }
1799         int left = getParagraphLeft(line);
1800         int right = getParagraphRight(line);
1801 
1802         int lineStartPos = getLineStartPos(line, left, right);
1803         float[] horizontal = new float[end - start + 1];
1804         for (int offset = 0; offset < horizontal.length; ++offset) {
1805             horizontal[offset] = lineStartPos + wid[offset];
1806         }
1807         return horizontal;
1808     }
1809 
fillHorizontalBoundsForLine(int line, float[] horizontalBounds)1810     private void fillHorizontalBoundsForLine(int line, float[] horizontalBounds) {
1811         final int lineStart = getLineStart(line);
1812         final int lineEnd = getLineEnd(line);
1813         final int lineLength = lineEnd - lineStart;
1814 
1815         final int dir = getParagraphDirection(line);
1816         final Directions directions = getLineDirections(line);
1817 
1818         final boolean hasTab = getLineContainsTab(line);
1819         TabStops tabStops = null;
1820         if (hasTab && mText instanceof Spanned) {
1821             // Just checking this line should be good enough, tabs should be
1822             // consistent across all lines in a paragraph.
1823             TabStopSpan[] tabs =
1824                     getParagraphSpans((Spanned) mText, lineStart, lineEnd, TabStopSpan.class);
1825             if (tabs.length > 0) {
1826                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
1827             }
1828         }
1829 
1830         final TextLine tl = TextLine.obtain();
1831         tl.set(mPaint, mText, lineStart, lineEnd, dir, directions, hasTab, tabStops,
1832                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
1833                 isFallbackLineSpacingEnabled());
1834         if (horizontalBounds == null || horizontalBounds.length < 2 * lineLength) {
1835             horizontalBounds = new float[2 * lineLength];
1836         }
1837 
1838         tl.measureAllBounds(horizontalBounds, null);
1839         TextLine.recycle(tl);
1840     }
1841 
1842     /**
1843      * Return the characters' bounds in the given range. The {@code bounds} array will be filled
1844      * starting from {@code boundsStart} (inclusive). The coordinates are in local text layout.
1845      *
1846      * @param start the start index to compute the character bounds, inclusive.
1847      * @param end the end index to compute the character bounds, exclusive.
1848      * @param bounds the array to fill in the character bounds. The array is divided into segments
1849      *               of four where each index in that segment represents left, top, right and
1850      *               bottom of the character.
1851      * @param boundsStart the inclusive start index in the array to start filling in the values
1852      *                    from.
1853      *
1854      * @throws IndexOutOfBoundsException if the range defined by {@code start} and {@code end}
1855      * exceeds the range of the text, or {@code bounds} doesn't have enough space to store the
1856      * result.
1857      * @throws IllegalArgumentException if {@code bounds} is null.
1858      */
fillCharacterBounds(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull float[] bounds, @IntRange(from = 0) int boundsStart)1859     public void fillCharacterBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
1860             @NonNull float[] bounds, @IntRange(from = 0) int boundsStart) {
1861         if (start < 0 || end < start || end > mText.length()) {
1862             throw new IndexOutOfBoundsException("given range: " + start + ", " + end + " is "
1863                     + "out of the text range: 0, " + mText.length());
1864         }
1865 
1866         if (bounds == null) {
1867             throw new IllegalArgumentException("bounds can't be null.");
1868         }
1869 
1870         final int neededLength = 4 * (end - start);
1871         if (neededLength > bounds.length - boundsStart) {
1872             throw new IndexOutOfBoundsException("bounds doesn't have enough space to store the "
1873                     + "result, needed: " + neededLength + " had: "
1874                     + (bounds.length - boundsStart));
1875         }
1876 
1877         if (start == end) {
1878             return;
1879         }
1880 
1881         final int startLine = getLineForOffset(start);
1882         final int endLine = getLineForOffset(end - 1);
1883 
1884         forEachCharacterBounds(start, end, startLine, endLine,
1885                 (index, lineNum, left, lineTop, right, lineBottom) -> {
1886                     final int boundsIndex = boundsStart + 4 * (index - start);
1887                     bounds[boundsIndex] = left;
1888                     bounds[boundsIndex + 1] = lineTop;
1889                     bounds[boundsIndex + 2] = right;
1890                     bounds[boundsIndex + 3] = lineBottom;
1891                 });
1892     }
1893 
1894     /**
1895      * Return the characters' bounds in the given range. The coordinates are in local text layout.
1896      *
1897      * @param start the start index to compute the character bounds, inclusive.
1898      * @param end the end index to compute the character bounds, exclusive.
1899      * @param startLine index of the line that contains {@code start}
1900      * @param endLine index of the line that contains {@code end}
1901      * @param listener called for each character with its bounds
1902      *
1903      */
forEachCharacterBounds( @ntRangefrom = 0) int start, @IntRange(from = 0) int end, @IntRange(from = 0) int startLine, @IntRange(from = 0) int endLine, CharacterBoundsListener listener )1904     private void forEachCharacterBounds(
1905             @IntRange(from = 0) int start,
1906             @IntRange(from = 0) int end,
1907             @IntRange(from = 0) int startLine,
1908             @IntRange(from = 0) int endLine,
1909             CharacterBoundsListener listener
1910     ) {
1911         float[] horizontalBounds = null;
1912         for (int line = startLine; line <= endLine; ++line) {
1913             final int lineStart = getLineStart(line);
1914             final int lineEnd = getLineEnd(line);
1915             final int lineLength = lineEnd - lineStart;
1916             if (horizontalBounds == null || horizontalBounds.length < 2 * lineLength) {
1917                 horizontalBounds = new float[2 * lineLength];
1918             }
1919             fillHorizontalBoundsForLine(line, horizontalBounds);
1920 
1921             final int lineLeft = getParagraphLeft(line);
1922             final int lineRight = getParagraphRight(line);
1923             final int lineStartPos = getLineStartPos(line, lineLeft, lineRight);
1924 
1925             final int lineTop = getLineTop(line);
1926             final int lineBottom = getLineBottom(line);
1927 
1928             final int startIndex = Math.max(start, lineStart);
1929             final int endIndex = Math.min(end, lineEnd);
1930             for (int index = startIndex; index < endIndex; ++index) {
1931                 final int offset = index - lineStart;
1932                 final float left = horizontalBounds[offset * 2] + lineStartPos;
1933                 final float right = horizontalBounds[offset * 2 + 1] + lineStartPos;
1934 
1935                 listener.onCharacterBounds(index, line, left, lineTop, right, lineBottom);
1936             }
1937         }
1938         listener.onEnd();
1939     }
1940 
1941     /**
1942      * Get the leftmost position that should be exposed for horizontal
1943      * scrolling on the specified line.
1944      */
getLineLeft(int line)1945     public float getLineLeft(int line) {
1946         final int dir = getParagraphDirection(line);
1947         Alignment align = getParagraphAlignment(line);
1948         // Before Q, StaticLayout.Builder.setAlignment didn't check whether the input alignment
1949         // is null. And when it is null, the old behavior is the same as ALIGN_CENTER.
1950         // To keep consistency, we convert a null alignment to ALIGN_CENTER.
1951         if (align == null) {
1952             align = Alignment.ALIGN_CENTER;
1953         }
1954 
1955         // First convert combinations of alignment and direction settings to
1956         // three basic cases: ALIGN_LEFT, ALIGN_RIGHT and ALIGN_CENTER.
1957         // For unexpected cases, it will fallback to ALIGN_LEFT.
1958         final Alignment resultAlign;
1959         switch(align) {
1960             case ALIGN_NORMAL:
1961                 resultAlign =
1962                         dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_RIGHT : Alignment.ALIGN_LEFT;
1963                 break;
1964             case ALIGN_OPPOSITE:
1965                 resultAlign =
1966                         dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_LEFT : Alignment.ALIGN_RIGHT;
1967                 break;
1968             case ALIGN_CENTER:
1969                 resultAlign = Alignment.ALIGN_CENTER;
1970                 break;
1971             case ALIGN_RIGHT:
1972                 resultAlign = Alignment.ALIGN_RIGHT;
1973                 break;
1974             default: /* align == Alignment.ALIGN_LEFT */
1975                 resultAlign = Alignment.ALIGN_LEFT;
1976         }
1977 
1978         // Here we must use getLineMax() to do the computation, because it maybe overridden by
1979         // derived class. And also note that line max equals the width of the text in that line
1980         // plus the leading margin.
1981         switch (resultAlign) {
1982             case ALIGN_CENTER:
1983                 final int left = getParagraphLeft(line);
1984                 final float max = getLineMax(line);
1985                 // This computation only works when mWidth equals leadingMargin plus
1986                 // the width of text in this line. If this condition doesn't meet anymore,
1987                 // please change here too.
1988                 return (float) Math.floor(left + (mWidth - max) / 2);
1989             case ALIGN_RIGHT:
1990                 return mWidth - getLineMax(line);
1991             default: /* resultAlign == Alignment.ALIGN_LEFT */
1992                 return 0;
1993         }
1994     }
1995 
1996     /**
1997      * Get the rightmost position that should be exposed for horizontal
1998      * scrolling on the specified line.
1999      */
getLineRight(int line)2000     public float getLineRight(int line) {
2001         final int dir = getParagraphDirection(line);
2002         Alignment align = getParagraphAlignment(line);
2003         // Before Q, StaticLayout.Builder.setAlignment didn't check whether the input alignment
2004         // is null. And when it is null, the old behavior is the same as ALIGN_CENTER.
2005         // To keep consistency, we convert a null alignment to ALIGN_CENTER.
2006         if (align == null) {
2007             align = Alignment.ALIGN_CENTER;
2008         }
2009 
2010         final Alignment resultAlign;
2011         switch(align) {
2012             case ALIGN_NORMAL:
2013                 resultAlign =
2014                         dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_RIGHT : Alignment.ALIGN_LEFT;
2015                 break;
2016             case ALIGN_OPPOSITE:
2017                 resultAlign =
2018                         dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_LEFT : Alignment.ALIGN_RIGHT;
2019                 break;
2020             case ALIGN_CENTER:
2021                 resultAlign = Alignment.ALIGN_CENTER;
2022                 break;
2023             case ALIGN_RIGHT:
2024                 resultAlign = Alignment.ALIGN_RIGHT;
2025                 break;
2026             default: /* align == Alignment.ALIGN_LEFT */
2027                 resultAlign = Alignment.ALIGN_LEFT;
2028         }
2029 
2030         switch (resultAlign) {
2031             case ALIGN_CENTER:
2032                 final int right = getParagraphRight(line);
2033                 final float max = getLineMax(line);
2034                 // This computation only works when mWidth equals leadingMargin plus width of the
2035                 // text in this line. If this condition doesn't meet anymore, please change here.
2036                 return (float) Math.ceil(right - (mWidth - max) / 2);
2037             case ALIGN_RIGHT:
2038                 return mWidth;
2039             default: /* resultAlign == Alignment.ALIGN_LEFT */
2040                 return getLineMax(line);
2041         }
2042     }
2043 
2044     /**
2045      * Gets the unsigned horizontal extent of the specified line, including
2046      * leading margin indent, but excluding trailing whitespace.
2047      */
getLineMax(int line)2048     public float getLineMax(int line) {
2049         float margin = getParagraphLeadingMargin(line);
2050         float signedExtent = getLineExtent(line, false);
2051         return margin + (signedExtent >= 0 ? signedExtent : -signedExtent);
2052     }
2053 
2054     /**
2055      * Gets the unsigned horizontal extent of the specified line, including
2056      * leading margin indent and trailing whitespace.
2057      */
getLineWidth(int line)2058     public float getLineWidth(int line) {
2059         float margin = getParagraphLeadingMargin(line);
2060         float signedExtent = getLineExtent(line, true);
2061         return margin + (signedExtent >= 0 ? signedExtent : -signedExtent);
2062     }
2063 
2064     /**
2065      * Like {@link #getLineExtent(int,TabStops,boolean)} but determines the
2066      * tab stops instead of using the ones passed in.
2067      * @param line the index of the line
2068      * @param full whether to include trailing whitespace
2069      * @return the extent of the line
2070      */
getLineExtent(int line, boolean full)2071     private float getLineExtent(int line, boolean full) {
2072         final int start = getLineStart(line);
2073         final int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
2074 
2075         final boolean hasTabs = getLineContainsTab(line);
2076         TabStops tabStops = null;
2077         if (hasTabs && mText instanceof Spanned) {
2078             // Just checking this line should be good enough, tabs should be
2079             // consistent across all lines in a paragraph.
2080             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
2081             if (tabs.length > 0) {
2082                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
2083             }
2084         }
2085         final Directions directions = getLineDirections(line);
2086         // Returned directions can actually be null
2087         if (directions == null) {
2088             return 0f;
2089         }
2090         final int dir = getParagraphDirection(line);
2091 
2092         final TextLine tl = TextLine.obtain();
2093         final TextPaint paint = mWorkPaint;
2094         paint.set(mPaint);
2095         paint.setStartHyphenEdit(getStartHyphenEdit(line));
2096         paint.setEndHyphenEdit(getEndHyphenEdit(line));
2097         tl.set(paint, mText, start, end, dir, directions, hasTabs, tabStops,
2098                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
2099                 isFallbackLineSpacingEnabled());
2100         if (isJustificationRequired(line)) {
2101             tl.justify(mJustificationMode, getJustifyWidth(line));
2102         }
2103         final float width = tl.metrics(null, null, mUseBoundsForWidth, null);
2104         TextLine.recycle(tl);
2105         return width;
2106     }
2107 
2108     /**
2109      * Returns the number of letter spacing unit in the line.
2110      *
2111      * <p>
2112      * This API returns a number of letters that is a target of letter spacing. The letter spacing
2113      * won't be added to the middle of the characters that are needed to be treated as a single,
2114      * e.g., ligatured or conjunct form. Note that this value is different from the number of]
2115      * grapheme clusters that is calculated by {@link BreakIterator#getCharacterInstance(Locale)}.
2116      * For example, if the "fi" is ligatured, the ligatured form is treated as single uni and letter
2117      * spacing is not added, but it has two separate grapheme cluster.
2118      *
2119      * <p>
2120      * This value is used for calculating the letter spacing amount for the justification because
2121      * the letter spacing is applied between clusters. For example, if extra {@code W} pixels needed
2122      * to be filled by letter spacing, the amount of letter spacing to be applied is
2123      * {@code W}/(letter spacing unit count - 1) px.
2124      *
2125      * @param line the index of the line
2126      * @param includeTrailingWhitespace whether to include trailing whitespace
2127      * @return the number of cluster count in the line.
2128      */
2129     @IntRange(from = 0)
2130     @FlaggedApi(FLAG_LETTER_SPACING_JUSTIFICATION)
getLineLetterSpacingUnitCount(@ntRangefrom = 0) int line, boolean includeTrailingWhitespace)2131     public int getLineLetterSpacingUnitCount(@IntRange(from = 0) int line,
2132             boolean includeTrailingWhitespace) {
2133         final int start = getLineStart(line);
2134         final int end = includeTrailingWhitespace ? getLineEnd(line)
2135                 : getLineVisibleEnd(line, getLineStart(line), getLineStart(line + 1),
2136                         false  // trailingSpaceAtLastLineIsVisible: Treating trailing whitespaces at
2137                                // the last line as a invisible chars for single line justification.
2138                 );
2139 
2140         final Directions directions = getLineDirections(line);
2141         // Returned directions can actually be null
2142         if (directions == null) {
2143             return 0;
2144         }
2145         final int dir = getParagraphDirection(line);
2146 
2147         final TextLine tl = TextLine.obtain();
2148         final TextPaint paint = mWorkPaint;
2149         paint.set(mPaint);
2150         paint.setStartHyphenEdit(getStartHyphenEdit(line));
2151         paint.setEndHyphenEdit(getEndHyphenEdit(line));
2152         tl.set(paint, mText, start, end, dir, directions,
2153                 false, null, // tab width is not used for cluster counting.
2154                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
2155                 isFallbackLineSpacingEnabled());
2156         if (mLineInfo == null) {
2157             mLineInfo = new TextLine.LineInfo();
2158         }
2159         mLineInfo.setClusterCount(0);
2160         tl.metrics(null, null, mUseBoundsForWidth, mLineInfo);
2161         TextLine.recycle(tl);
2162         return mLineInfo.getClusterCount();
2163     }
2164 
2165     /**
2166      * Returns the signed horizontal extent of the specified line, excluding
2167      * leading margin.  If full is false, excludes trailing whitespace.
2168      * @param line the index of the line
2169      * @param tabStops the tab stops, can be null if we know they're not used.
2170      * @param full whether to include trailing whitespace
2171      * @return the extent of the text on this line
2172      */
getLineExtent(int line, TabStops tabStops, boolean full)2173     private float getLineExtent(int line, TabStops tabStops, boolean full) {
2174         final int start = getLineStart(line);
2175         final int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
2176         final boolean hasTabs = getLineContainsTab(line);
2177         final Directions directions = getLineDirections(line);
2178         final int dir = getParagraphDirection(line);
2179 
2180         final TextLine tl = TextLine.obtain();
2181         final TextPaint paint = mWorkPaint;
2182         paint.set(mPaint);
2183         paint.setStartHyphenEdit(getStartHyphenEdit(line));
2184         paint.setEndHyphenEdit(getEndHyphenEdit(line));
2185         tl.set(paint, mText, start, end, dir, directions, hasTabs, tabStops,
2186                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
2187                 isFallbackLineSpacingEnabled());
2188         if (isJustificationRequired(line)) {
2189             tl.justify(mJustificationMode, getJustifyWidth(line));
2190         }
2191         final float width = tl.metrics(null, null, mUseBoundsForWidth, null);
2192         TextLine.recycle(tl);
2193         return width;
2194     }
2195 
2196     /**
2197      * Get the line number corresponding to the specified vertical position.
2198      * If you ask for a position above 0, you get 0; if you ask for a position
2199      * below the bottom of the text, you get the last line.
2200      */
2201     // FIXME: It may be faster to do a linear search for layouts without many lines.
getLineForVertical(int vertical)2202     public int getLineForVertical(int vertical) {
2203         int high = getLineCount(), low = -1, guess;
2204 
2205         while (high - low > 1) {
2206             guess = (high + low) / 2;
2207 
2208             if (getLineTop(guess) > vertical)
2209                 high = guess;
2210             else
2211                 low = guess;
2212         }
2213 
2214         if (low < 0)
2215             return 0;
2216         else
2217             return low;
2218     }
2219 
2220     /**
2221      * Get the line number on which the specified text offset appears.
2222      * If you ask for a position before 0, you get 0; if you ask for a position
2223      * beyond the end of the text, you get the last line.
2224      */
getLineForOffset(int offset)2225     public int getLineForOffset(int offset) {
2226         int high = getLineCount(), low = -1, guess;
2227 
2228         while (high - low > 1) {
2229             guess = (high + low) / 2;
2230 
2231             if (getLineStart(guess) > offset)
2232                 high = guess;
2233             else
2234                 low = guess;
2235         }
2236 
2237         if (low < 0) {
2238             return 0;
2239         } else {
2240             return low;
2241         }
2242     }
2243 
2244     /**
2245      * Get the character offset on the specified line whose position is
2246      * closest to the specified horizontal position.
2247      */
getOffsetForHorizontal(int line, float horiz)2248     public int getOffsetForHorizontal(int line, float horiz) {
2249         return getOffsetForHorizontal(line, horiz, true);
2250     }
2251 
2252     /**
2253      * Get the character offset on the specified line whose position is
2254      * closest to the specified horizontal position.
2255      *
2256      * @param line the line used to find the closest offset
2257      * @param horiz the horizontal position used to find the closest offset
2258      * @param primary whether to use the primary position or secondary position to find the offset
2259      *
2260      * @hide
2261      */
getOffsetForHorizontal(int line, float horiz, boolean primary)2262     public int getOffsetForHorizontal(int line, float horiz, boolean primary) {
2263         // TODO: use Paint.getOffsetForAdvance to avoid binary search
2264         final int lineEndOffset = getLineEnd(line);
2265         final int lineStartOffset = getLineStart(line);
2266 
2267         Directions dirs = getLineDirections(line);
2268 
2269         TextLine tl = TextLine.obtain();
2270         // XXX: we don't care about tabs as we just use TextLine#getOffsetToLeftRightOf here.
2271         tl.set(mPaint, mText, lineStartOffset, lineEndOffset, getParagraphDirection(line), dirs,
2272                 false, null,
2273                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
2274                 isFallbackLineSpacingEnabled());
2275         final HorizontalMeasurementProvider horizontal =
2276                 new HorizontalMeasurementProvider(line, primary);
2277 
2278         final int max;
2279         if (line == getLineCount() - 1) {
2280             max = lineEndOffset;
2281         } else {
2282             max = tl.getOffsetToLeftRightOf(lineEndOffset - lineStartOffset,
2283                     !isRtlCharAt(lineEndOffset - 1)) + lineStartOffset;
2284         }
2285         int best = lineStartOffset;
2286         float bestdist = Math.abs(horizontal.get(lineStartOffset) - horiz);
2287 
2288         for (int i = 0; i < dirs.mDirections.length; i += 2) {
2289             int here = lineStartOffset + dirs.mDirections[i];
2290             int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK);
2291             boolean isRtl = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0;
2292             int swap = isRtl ? -1 : 1;
2293 
2294             if (there > max)
2295                 there = max;
2296             int high = there - 1 + 1, low = here + 1 - 1, guess;
2297 
2298             while (high - low > 1) {
2299                 guess = (high + low) / 2;
2300                 int adguess = getOffsetAtStartOf(guess);
2301 
2302                 if (horizontal.get(adguess) * swap >= horiz * swap) {
2303                     high = guess;
2304                 } else {
2305                     low = guess;
2306                 }
2307             }
2308 
2309             if (low < here + 1)
2310                 low = here + 1;
2311 
2312             if (low < there) {
2313                 int aft = tl.getOffsetToLeftRightOf(low - lineStartOffset, isRtl) + lineStartOffset;
2314                 low = tl.getOffsetToLeftRightOf(aft - lineStartOffset, !isRtl) + lineStartOffset;
2315                 if (low >= here && low < there) {
2316                     float dist = Math.abs(horizontal.get(low) - horiz);
2317                     if (aft < there) {
2318                         float other = Math.abs(horizontal.get(aft) - horiz);
2319 
2320                         if (other < dist) {
2321                             dist = other;
2322                             low = aft;
2323                         }
2324                     }
2325 
2326                     if (dist < bestdist) {
2327                         bestdist = dist;
2328                         best = low;
2329                     }
2330                 }
2331             }
2332 
2333             float dist = Math.abs(horizontal.get(here) - horiz);
2334 
2335             if (dist < bestdist) {
2336                 bestdist = dist;
2337                 best = here;
2338             }
2339         }
2340 
2341         float dist = Math.abs(horizontal.get(max) - horiz);
2342 
2343         if (dist <= bestdist) {
2344             best = max;
2345         }
2346 
2347         TextLine.recycle(tl);
2348         return best;
2349     }
2350 
2351     /**
2352      * Responds to #getHorizontal queries, by selecting the better strategy between:
2353      * - calling #getHorizontal explicitly for each query
2354      * - precomputing all #getHorizontal measurements, and responding to any query in constant time
2355      * The first strategy is used for LTR-only text, while the second is used for all other cases.
2356      * The class is currently only used in #getOffsetForHorizontal, so reuse with care in other
2357      * contexts.
2358      */
2359     private class HorizontalMeasurementProvider {
2360         private final int mLine;
2361         private final boolean mPrimary;
2362 
2363         private float[] mHorizontals;
2364         private int mLineStartOffset;
2365 
HorizontalMeasurementProvider(final int line, final boolean primary)2366         HorizontalMeasurementProvider(final int line, final boolean primary) {
2367             mLine = line;
2368             mPrimary = primary;
2369             init();
2370         }
2371 
init()2372         private void init() {
2373             final Directions dirs = getLineDirections(mLine);
2374             if (dirs == DIRS_ALL_LEFT_TO_RIGHT) {
2375                 return;
2376             }
2377 
2378             mHorizontals = getLineHorizontals(mLine, false, mPrimary);
2379             mLineStartOffset = getLineStart(mLine);
2380         }
2381 
get(final int offset)2382         float get(final int offset) {
2383             final int index = offset - mLineStartOffset;
2384             if (mHorizontals == null || index < 0 || index >= mHorizontals.length) {
2385                 return getHorizontal(offset, mPrimary);
2386             } else {
2387                 return mHorizontals[index];
2388             }
2389         }
2390     }
2391 
2392     /**
2393      * Finds the range of text which is inside the specified rectangle area. The start of the range
2394      * is the start of the first text segment inside the area, and the end of the range is the end
2395      * of the last text segment inside the area.
2396      *
2397      * <p>A text segment is considered to be inside the area according to the provided {@link
2398      * TextInclusionStrategy}. If a text segment spans multiple lines or multiple directional runs
2399      * (e.g. a hyphenated word), the text segment is divided into pieces at the line and run breaks,
2400      * then the text segment is considered to be inside the area if any of its pieces are inside the
2401      * area.
2402      *
2403      * <p>The returned range may also include text segments which are not inside the specified area,
2404      * if those text segments are in between text segments which are inside the area. For example,
2405      * the returned range may be "segment1 segment2 segment3" if "segment1" and "segment3" are
2406      * inside the area and "segment2" is not.
2407      *
2408      * @param area area for which the text range will be found
2409      * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a
2410      *     text segment
2411      * @param inclusionStrategy strategy for determining whether a text segment is inside the
2412      *     specified area
2413      * @return int array of size 2 containing the start (inclusive) and end (exclusive) character
2414      *     offsets of the text range, or null if there are no text segments inside the area
2415      */
2416     @Nullable
getRangeForRect(@onNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull TextInclusionStrategy inclusionStrategy)2417     public int[] getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder,
2418             @NonNull TextInclusionStrategy inclusionStrategy) {
2419         // Find the first line whose bottom (without line spacing) is below the top of the area.
2420         int startLine = getLineForVertical((int) area.top);
2421         if (area.top > getLineBottom(startLine, /* includeLineSpacing= */ false)) {
2422             startLine++;
2423             if (startLine >= getLineCount()) {
2424                 // The entire area is below the last line, so it does not contain any text.
2425                 return null;
2426             }
2427         }
2428 
2429         // Find the last line whose top is above the bottom of the area.
2430         int endLine = getLineForVertical((int) area.bottom);
2431         if (endLine == 0 && area.bottom < getLineTop(0)) {
2432             // The entire area is above the first line, so it does not contain any text.
2433             return null;
2434         }
2435         if (endLine < startLine) {
2436             // The entire area is between two lines, so it does not contain any text.
2437             return null;
2438         }
2439 
2440         int start = getStartOrEndOffsetForAreaWithinLine(
2441                 startLine, area, segmentFinder, inclusionStrategy, /* getStart= */ true);
2442         // If the area does not contain any text on this line, keep trying subsequent lines until
2443         // the end line is reached.
2444         while (start == -1 && startLine < endLine) {
2445             startLine++;
2446             start = getStartOrEndOffsetForAreaWithinLine(
2447                     startLine, area, segmentFinder, inclusionStrategy, /* getStart= */ true);
2448         }
2449         if (start == -1) {
2450             // All lines were checked, the area does not contain any text.
2451             return null;
2452         }
2453 
2454         int end = getStartOrEndOffsetForAreaWithinLine(
2455                 endLine, area, segmentFinder, inclusionStrategy, /* getStart= */ false);
2456         // If the area does not contain any text on this line, keep trying previous lines until
2457         // the start line is reached.
2458         while (end == -1 && startLine < endLine) {
2459             endLine--;
2460             end = getStartOrEndOffsetForAreaWithinLine(
2461                     endLine, area, segmentFinder, inclusionStrategy, /* getStart= */ false);
2462         }
2463         if (end == -1) {
2464             // All lines were checked, the area does not contain any text.
2465             return null;
2466         }
2467 
2468         // If a text segment spans multiple lines or multiple directional runs (e.g. a hyphenated
2469         // word), then getStartOrEndOffsetForAreaWithinLine() can return an offset in the middle of
2470         // a text segment. Adjust the range to include the rest of any partial text segments. If
2471         // start is already the start boundary of a text segment, then this is a no-op.
2472         start = segmentFinder.previousStartBoundary(start + 1);
2473         end = segmentFinder.nextEndBoundary(end - 1);
2474 
2475         return new int[] {start, end};
2476     }
2477 
2478     /**
2479      * Finds the start character offset of the first text segment within a line inside the specified
2480      * rectangle area, or the end character offset of the last text segment inside the area.
2481      *
2482      * @param line index of the line to search
2483      * @param area area inside which text segments will be found
2484      * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a
2485      *     text segment
2486      * @param inclusionStrategy strategy for determining whether a text segment is inside the
2487      *     specified area
2488      * @param getStart true to find the start of the first text segment inside the area, false to
2489      *     find the end of the last text segment
2490      * @return the start character offset of the first text segment inside the area, or the end
2491      *     character offset of the last text segment inside the area.
2492      */
getStartOrEndOffsetForAreaWithinLine( @ntRangefrom = 0) int line, @NonNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull TextInclusionStrategy inclusionStrategy, boolean getStart)2493     private int getStartOrEndOffsetForAreaWithinLine(
2494             @IntRange(from = 0) int line,
2495             @NonNull RectF area,
2496             @NonNull SegmentFinder segmentFinder,
2497             @NonNull TextInclusionStrategy inclusionStrategy,
2498             boolean getStart) {
2499         int lineTop = getLineTop(line);
2500         int lineBottom = getLineBottom(line, /* includeLineSpacing= */ false);
2501 
2502         int lineStartOffset = getLineStart(line);
2503         int lineEndOffset = getLineEnd(line);
2504         if (lineStartOffset == lineEndOffset) {
2505             return -1;
2506         }
2507 
2508         float[] horizontalBounds = new float[2 * (lineEndOffset - lineStartOffset)];
2509         fillHorizontalBoundsForLine(line, horizontalBounds);
2510 
2511         int lineStartPos = getLineStartPos(line, getParagraphLeft(line), getParagraphRight(line));
2512 
2513         // Loop through the runs forwards or backwards depending on getStart value.
2514         Layout.Directions directions = getLineDirections(line);
2515         int runIndex = getStart ? 0 : directions.getRunCount() - 1;
2516         while ((getStart && runIndex < directions.getRunCount()) || (!getStart && runIndex >= 0)) {
2517             // runStartOffset and runEndOffset are offset indices within the line.
2518             int runStartOffset = directions.getRunStart(runIndex);
2519             int runEndOffset = Math.min(
2520                     runStartOffset + directions.getRunLength(runIndex),
2521                     lineEndOffset - lineStartOffset);
2522             boolean isRtl = directions.isRunRtl(runIndex);
2523             float runLeft = lineStartPos
2524                     + (isRtl
2525                             ? horizontalBounds[2 * (runEndOffset - 1)]
2526                             : horizontalBounds[2 * runStartOffset]);
2527             float runRight = lineStartPos
2528                     + (isRtl
2529                             ? horizontalBounds[2 * runStartOffset + 1]
2530                             : horizontalBounds[2 * (runEndOffset - 1) + 1]);
2531 
2532             int result =
2533                     getStart
2534                             ? getStartOffsetForAreaWithinRun(
2535                                     area, lineTop, lineBottom,
2536                                     lineStartOffset, lineStartPos, horizontalBounds,
2537                                     runStartOffset, runEndOffset, runLeft, runRight, isRtl,
2538                                     segmentFinder, inclusionStrategy)
2539                             : getEndOffsetForAreaWithinRun(
2540                                     area, lineTop, lineBottom,
2541                                     lineStartOffset, lineStartPos, horizontalBounds,
2542                                     runStartOffset, runEndOffset, runLeft, runRight, isRtl,
2543                                     segmentFinder, inclusionStrategy);
2544             if (result >= 0) {
2545                 return result;
2546             }
2547 
2548             runIndex += getStart ? 1 : -1;
2549         }
2550         return -1;
2551     }
2552 
2553     /**
2554      * Finds the start character offset of the first text segment within a directional run inside
2555      * the specified rectangle area.
2556      *
2557      * @param area area inside which text segments will be found
2558      * @param lineTop top of the line containing this run
2559      * @param lineBottom bottom (not including line spacing) of the line containing this run
2560      * @param lineStartOffset start character offset of the line containing this run
2561      * @param lineStartPos start position of the line containing this run
2562      * @param horizontalBounds array containing the signed horizontal bounds of the characters in
2563      *     the line. The left and right bounds of the character at offset i are stored at index (2 *
2564      *     i) and index (2 * i + 1). Bounds are relative to {@code lineStartPos}.
2565      * @param runStartOffset start offset of the run relative to {@code lineStartOffset}
2566      * @param runEndOffset end offset of the run relative to {@code lineStartOffset}
2567      * @param runLeft left bound of the run
2568      * @param runRight right bound of the run
2569      * @param isRtl whether the run is right-to-left
2570      * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a
2571      *     text segment
2572      * @param inclusionStrategy strategy for determining whether a text segment is inside the
2573      *     specified area
2574      * @return the start character offset of the first text segment inside the area
2575      */
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)2576     private static int getStartOffsetForAreaWithinRun(
2577             @NonNull RectF area,
2578             int lineTop, int lineBottom,
2579             @IntRange(from = 0) int lineStartOffset,
2580             @IntRange(from = 0) int lineStartPos,
2581             @NonNull float[] horizontalBounds,
2582             @IntRange(from = 0) int runStartOffset, @IntRange(from = 0) int runEndOffset,
2583             float runLeft, float runRight,
2584             boolean isRtl,
2585             @NonNull SegmentFinder segmentFinder,
2586             @NonNull TextInclusionStrategy inclusionStrategy) {
2587         if (runRight < area.left || runLeft > area.right) {
2588             // The run does not overlap the area.
2589             return -1;
2590         }
2591 
2592         // Find the first character in the run whose bounds overlap with the area.
2593         // firstCharOffset is an offset index within the line.
2594         int firstCharOffset;
2595         if ((!isRtl && area.left <= runLeft) || (isRtl && area.right >= runRight)) {
2596             firstCharOffset = runStartOffset;
2597         } else {
2598             int low = runStartOffset;
2599             int high = runEndOffset;
2600             int guess;
2601             while (high - low > 1) {
2602                 guess = (high + low) / 2;
2603                 // Left edge of the character at guess
2604                 float pos = lineStartPos + horizontalBounds[2 * guess];
2605                 if ((!isRtl && pos > area.left) || (isRtl && pos < area.right)) {
2606                     high = guess;
2607                 } else {
2608                     low = guess;
2609                 }
2610             }
2611             // The area edge is between the left edge of the character at low and the left edge of
2612             // the character at high. For LTR text, this is within the character at low. For RTL
2613             // text, this is within the character at high.
2614             firstCharOffset = isRtl ? high : low;
2615         }
2616 
2617         // Find the first text segment containing this character (or, if no text segment contains
2618         // this character, the first text segment after this character). All previous text segments
2619         // in this run are to the left (for LTR) of the area.
2620         int segmentEndOffset =
2621                 segmentFinder.nextEndBoundary(lineStartOffset + firstCharOffset);
2622         if (segmentEndOffset == SegmentFinder.DONE) {
2623             // There are no text segments containing or after firstCharOffset, so no text segments
2624             // in this run overlap the area.
2625             return -1;
2626         }
2627         int segmentStartOffset = segmentFinder.previousStartBoundary(segmentEndOffset);
2628         if (segmentStartOffset >= lineStartOffset + runEndOffset) {
2629             // The text segment is after the end of this run, so no text segments in this run
2630             // overlap the area.
2631             return -1;
2632         }
2633         // If the segment extends outside of this run, only consider the piece of the segment within
2634         // this run.
2635         segmentStartOffset = Math.max(segmentStartOffset, lineStartOffset + runStartOffset);
2636         segmentEndOffset = Math.min(segmentEndOffset, lineStartOffset + runEndOffset);
2637 
2638         RectF segmentBounds = new RectF(0, lineTop, 0, lineBottom);
2639         while (true) {
2640             // Start (left for LTR, right for RTL) edge of the character at segmentStartOffset.
2641             float segmentStart = lineStartPos + horizontalBounds[
2642                     2 * (segmentStartOffset - lineStartOffset) + (isRtl ? 1 : 0)];
2643             if ((!isRtl && segmentStart > area.right) || (isRtl && segmentStart < area.left)) {
2644                 // The entire area is to the left (for LTR) of the text segment. So the area does
2645                 // not contain any text segments within this run.
2646                 return -1;
2647             }
2648             // End (right for LTR, left for RTL) edge of the character at (segmentStartOffset - 1).
2649             float segmentEnd = lineStartPos + horizontalBounds[
2650                     2 * (segmentEndOffset - lineStartOffset - 1) + (isRtl ? 0 : 1)];
2651             segmentBounds.left = isRtl ? segmentEnd : segmentStart;
2652             segmentBounds.right = isRtl ? segmentStart : segmentEnd;
2653             if (inclusionStrategy.isSegmentInside(segmentBounds, area)) {
2654                 return segmentStartOffset;
2655             }
2656             // Try the next text segment.
2657             segmentStartOffset = segmentFinder.nextStartBoundary(segmentStartOffset);
2658             if (segmentStartOffset == SegmentFinder.DONE
2659                     || segmentStartOffset >= lineStartOffset + runEndOffset) {
2660                 // No more text segments within this run.
2661                 return -1;
2662             }
2663             segmentEndOffset = segmentFinder.nextEndBoundary(segmentStartOffset);
2664             // If the segment extends past the end of this run, only consider the piece of the
2665             // segment within this run.
2666             segmentEndOffset = Math.min(segmentEndOffset, lineStartOffset + runEndOffset);
2667         }
2668     }
2669 
2670     /**
2671      * Finds the end character offset of the last text segment within a directional run inside the
2672      * specified rectangle area.
2673      *
2674      * @param area area inside which text segments will be found
2675      * @param lineTop top of the line containing this run
2676      * @param lineBottom bottom (not including line spacing) of the line containing this run
2677      * @param lineStartOffset start character offset of the line containing this run
2678      * @param lineStartPos start position of the line containing this run
2679      * @param horizontalBounds array containing the signed horizontal bounds of the characters in
2680      *     the line. The left and right bounds of the character at offset i are stored at index (2 *
2681      *     i) and index (2 * i + 1). Bounds are relative to {@code lineStartPos}.
2682      * @param runStartOffset start offset of the run relative to {@code lineStartOffset}
2683      * @param runEndOffset end offset of the run relative to {@code lineStartOffset}
2684      * @param runLeft left bound of the run
2685      * @param runRight right bound of the run
2686      * @param isRtl whether the run is right-to-left
2687      * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a
2688      *     text segment
2689      * @param inclusionStrategy strategy for determining whether a text segment is inside the
2690      *     specified area
2691      * @return the end character offset of the last text segment inside the area
2692      */
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)2693     private static int getEndOffsetForAreaWithinRun(
2694             @NonNull RectF area,
2695             int lineTop, int lineBottom,
2696             @IntRange(from = 0) int lineStartOffset,
2697             @IntRange(from = 0) int lineStartPos,
2698             @NonNull float[] horizontalBounds,
2699             @IntRange(from = 0) int runStartOffset, @IntRange(from = 0) int runEndOffset,
2700             float runLeft, float runRight,
2701             boolean isRtl,
2702             @NonNull SegmentFinder segmentFinder,
2703             @NonNull TextInclusionStrategy inclusionStrategy) {
2704         if (runRight < area.left || runLeft > area.right) {
2705             // The run does not overlap the area.
2706             return -1;
2707         }
2708 
2709         // Find the last character in the run whose bounds overlap with the area.
2710         // firstCharOffset is an offset index within the line.
2711         int lastCharOffset;
2712         if ((!isRtl && area.right >= runRight) || (isRtl && area.left <= runLeft)) {
2713             lastCharOffset = runEndOffset - 1;
2714         } else {
2715             int low = runStartOffset;
2716             int high = runEndOffset;
2717             int guess;
2718             while (high - low > 1) {
2719                 guess = (high + low) / 2;
2720                 // Left edge of the character at guess
2721                 float pos = lineStartPos + horizontalBounds[2 * guess];
2722                 if ((!isRtl && pos > area.right) || (isRtl && pos < area.left)) {
2723                     high = guess;
2724                 } else {
2725                     low = guess;
2726                 }
2727             }
2728             // The area edge is between the left edge of the character at low and the left edge of
2729             // the character at high. For LTR text, this is within the character at low. For RTL
2730             // text, this is within the character at high.
2731             lastCharOffset = isRtl ? high : low;
2732         }
2733 
2734         // Find the last text segment containing this character (or, if no text segment contains
2735         // this character, the first text segment before this character). All following text
2736         // segments in this run are to the right (for LTR) of the area.
2737         // + 1 to allow segmentStartOffset = lineStartOffset + lastCharOffset
2738         int segmentStartOffset =
2739                 segmentFinder.previousStartBoundary(lineStartOffset + lastCharOffset + 1);
2740         if (segmentStartOffset == SegmentFinder.DONE) {
2741             // There are no text segments containing or before lastCharOffset, so no text segments
2742             // in this run overlap the area.
2743             return -1;
2744         }
2745         int segmentEndOffset = segmentFinder.nextEndBoundary(segmentStartOffset);
2746         if (segmentEndOffset <= lineStartOffset + runStartOffset) {
2747             // The text segment is before the start of this run, so no text segments in this run
2748             // overlap the area.
2749             return -1;
2750         }
2751         // If the segment extends outside of this run, only consider the piece of the segment within
2752         // this run.
2753         segmentStartOffset = Math.max(segmentStartOffset, lineStartOffset + runStartOffset);
2754         segmentEndOffset = Math.min(segmentEndOffset, lineStartOffset + runEndOffset);
2755 
2756         RectF segmentBounds = new RectF(0, lineTop, 0, lineBottom);
2757         while (true) {
2758             // End (right for LTR, left for RTL) edge of the character at (segmentStartOffset - 1).
2759             float segmentEnd = lineStartPos + horizontalBounds[
2760                     2 * (segmentEndOffset - lineStartOffset - 1) + (isRtl ? 0 : 1)];
2761             if ((!isRtl && segmentEnd < area.left) || (isRtl && segmentEnd > area.right)) {
2762                 // The entire area is to the right (for LTR) of the text segment. So the
2763                 // area does not contain any text segments within this run.
2764                 return -1;
2765             }
2766             // Start (left for LTR, right for RTL) edge of the character at segmentStartOffset.
2767             float segmentStart = lineStartPos + horizontalBounds[
2768                     2 * (segmentStartOffset - lineStartOffset) + (isRtl ? 1 : 0)];
2769             segmentBounds.left = isRtl ? segmentEnd : segmentStart;
2770             segmentBounds.right = isRtl ? segmentStart : segmentEnd;
2771             if (inclusionStrategy.isSegmentInside(segmentBounds, area)) {
2772                 return segmentEndOffset;
2773             }
2774             // Try the previous text segment.
2775             segmentEndOffset = segmentFinder.previousEndBoundary(segmentEndOffset);
2776             if (segmentEndOffset == SegmentFinder.DONE
2777                     || segmentEndOffset <= lineStartOffset + runStartOffset) {
2778                 // No more text segments within this run.
2779                 return -1;
2780             }
2781             segmentStartOffset = segmentFinder.previousStartBoundary(segmentEndOffset);
2782             // If the segment extends past the start of this run, only consider the piece of the
2783             // segment within this run.
2784             segmentStartOffset = Math.max(segmentStartOffset, lineStartOffset + runStartOffset);
2785         }
2786     }
2787 
2788     /**
2789      * Return the text offset after the last character on the specified line.
2790      */
getLineEnd(int line)2791     public final int getLineEnd(int line) {
2792         return getLineStart(line + 1);
2793     }
2794 
2795     /**
2796      * Return the text offset after the last visible character (so whitespace
2797      * is not counted) on the specified line.
2798      */
getLineVisibleEnd(int line)2799     public int getLineVisibleEnd(int line) {
2800         return getLineVisibleEnd(line, getLineStart(line), getLineStart(line + 1),
2801                 true /* trailingSpaceAtLastLineIsVisible */);
2802     }
2803 
getLineVisibleEnd(int line, int start, int end, boolean trailingSpaceAtLastLineIsVisible)2804     private int getLineVisibleEnd(int line, int start, int end,
2805             boolean trailingSpaceAtLastLineIsVisible) {
2806         CharSequence text = mText;
2807         char ch;
2808 
2809         // Historically, trailing spaces at the last line is counted as visible. However, this
2810         // doesn't work well for justification.
2811         if (trailingSpaceAtLastLineIsVisible) {
2812             if (line == getLineCount() - 1) {
2813                 return end;
2814             }
2815         }
2816 
2817         for (; end > start; end--) {
2818             ch = text.charAt(end - 1);
2819 
2820             if (ch == '\n') {
2821                 return end - 1;
2822             }
2823 
2824             if (!TextLine.isLineEndSpace(ch)) {
2825                 break;
2826             }
2827 
2828         }
2829 
2830         return end;
2831     }
2832 
2833     /**
2834      * Return the vertical position of the bottom of the specified line.
2835      */
getLineBottom(int line)2836     public final int getLineBottom(int line) {
2837         return getLineBottom(line, /* includeLineSpacing= */ true);
2838     }
2839 
2840     /**
2841      * Return the vertical position of the bottom of the specified line.
2842      *
2843      * @param line index of the line
2844      * @param includeLineSpacing whether to include the line spacing
2845      */
getLineBottom(int line, boolean includeLineSpacing)2846     public int getLineBottom(int line, boolean includeLineSpacing) {
2847         if (includeLineSpacing) {
2848             return getLineTop(line + 1);
2849         } else {
2850             return getLineTop(line + 1) - getLineExtra(line);
2851         }
2852     }
2853 
2854     /**
2855      * Return the vertical position of the baseline of the specified line.
2856      */
getLineBaseline(int line)2857     public final int getLineBaseline(int line) {
2858         // getLineTop(line+1) == getLineBottom(line)
2859         return getLineTop(line+1) - getLineDescent(line);
2860     }
2861 
2862     /**
2863      * Get the ascent of the text on the specified line.
2864      * The return value is negative to match the Paint.ascent() convention.
2865      */
getLineAscent(int line)2866     public final int getLineAscent(int line) {
2867         // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line)
2868         return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line));
2869     }
2870 
2871     /**
2872      * Return the extra space added as a result of line spacing attributes
2873      * {@link #getSpacingAdd()} and {@link #getSpacingMultiplier()}. Default value is {@code zero}.
2874      *
2875      * @param line the index of the line, the value should be equal or greater than {@code zero}
2876      * @hide
2877      */
getLineExtra(@ntRangefrom = 0) int line)2878     public int getLineExtra(@IntRange(from = 0) int line) {
2879         return 0;
2880     }
2881 
getOffsetToLeftOf(int offset)2882     public int getOffsetToLeftOf(int offset) {
2883         return getOffsetToLeftRightOf(offset, true);
2884     }
2885 
getOffsetToRightOf(int offset)2886     public int getOffsetToRightOf(int offset) {
2887         return getOffsetToLeftRightOf(offset, false);
2888     }
2889 
getOffsetToLeftRightOf(int caret, boolean toLeft)2890     private int getOffsetToLeftRightOf(int caret, boolean toLeft) {
2891         int line = getLineForOffset(caret);
2892         int lineStart = getLineStart(line);
2893         int lineEnd = getLineEnd(line);
2894         int lineDir = getParagraphDirection(line);
2895 
2896         boolean lineChanged = false;
2897         boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT);
2898         // if walking off line, look at the line we're headed to
2899         if (advance) {
2900             if (caret == lineEnd) {
2901                 if (line < getLineCount() - 1) {
2902                     lineChanged = true;
2903                     ++line;
2904                 } else {
2905                     return caret; // at very end, don't move
2906                 }
2907             }
2908         } else {
2909             if (caret == lineStart) {
2910                 if (line > 0) {
2911                     lineChanged = true;
2912                     --line;
2913                 } else {
2914                     return caret; // at very start, don't move
2915                 }
2916             }
2917         }
2918 
2919         if (lineChanged) {
2920             lineStart = getLineStart(line);
2921             lineEnd = getLineEnd(line);
2922             int newDir = getParagraphDirection(line);
2923             if (newDir != lineDir) {
2924                 // unusual case.  we want to walk onto the line, but it runs
2925                 // in a different direction than this one, so we fake movement
2926                 // in the opposite direction.
2927                 toLeft = !toLeft;
2928                 lineDir = newDir;
2929             }
2930         }
2931 
2932         Directions directions = getLineDirections(line);
2933 
2934         TextLine tl = TextLine.obtain();
2935         // XXX: we don't care about tabs
2936         tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null,
2937                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
2938                 isFallbackLineSpacingEnabled());
2939         caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft);
2940         TextLine.recycle(tl);
2941         return caret;
2942     }
2943 
getOffsetAtStartOf(int offset)2944     private int getOffsetAtStartOf(int offset) {
2945         // XXX this probably should skip local reorderings and
2946         // zero-width characters, look at callers
2947         if (offset == 0)
2948             return 0;
2949 
2950         CharSequence text = mText;
2951         char c = text.charAt(offset);
2952 
2953         if (c >= '\uDC00' && c <= '\uDFFF') {
2954             char c1 = text.charAt(offset - 1);
2955 
2956             if (c1 >= '\uD800' && c1 <= '\uDBFF')
2957                 offset -= 1;
2958         }
2959 
2960         if (mSpannedText) {
2961             ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
2962                                                        ReplacementSpan.class);
2963 
2964             for (int i = 0; i < spans.length; i++) {
2965                 int start = ((Spanned) text).getSpanStart(spans[i]);
2966                 int end = ((Spanned) text).getSpanEnd(spans[i]);
2967 
2968                 if (start < offset && end > offset)
2969                     offset = start;
2970             }
2971         }
2972 
2973         return offset;
2974     }
2975 
2976     /**
2977      * Determine whether we should clamp cursor position. Currently it's
2978      * only robust for left-aligned displays.
2979      * @hide
2980      */
2981     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
shouldClampCursor(int line)2982     public boolean shouldClampCursor(int line) {
2983         // Only clamp cursor position in left-aligned displays.
2984         switch (getParagraphAlignment(line)) {
2985             case ALIGN_LEFT:
2986                 return true;
2987             case ALIGN_NORMAL:
2988                 return getParagraphDirection(line) > 0;
2989             default:
2990                 return false;
2991         }
2992 
2993     }
2994 
2995     /**
2996      * Fills in the specified Path with a representation of a cursor
2997      * at the specified offset.  This will often be a vertical line
2998      * but can be multiple discontinuous lines in text with multiple
2999      * directionalities.
3000      */
getCursorPath(final int point, final Path dest, final CharSequence editingBuffer)3001     public void getCursorPath(final int point, final Path dest, final CharSequence editingBuffer) {
3002         dest.reset();
3003 
3004         int line = getLineForOffset(point);
3005         int top = getLineTop(line);
3006         int bottom = getLineBottom(line, /* includeLineSpacing= */ false);
3007 
3008         boolean clamped = shouldClampCursor(line);
3009         float h1 = getPrimaryHorizontal(point, clamped) - 0.5f;
3010 
3011         int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) |
3012                    TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING);
3013         int fn = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_ALT_ON);
3014         int dist = 0;
3015 
3016         if (caps != 0 || fn != 0) {
3017             dist = (bottom - top) >> 2;
3018 
3019             if (fn != 0)
3020                 top += dist;
3021             if (caps != 0)
3022                 bottom -= dist;
3023         }
3024 
3025         if (h1 < 0.5f)
3026             h1 = 0.5f;
3027 
3028         dest.moveTo(h1, top);
3029         dest.lineTo(h1, bottom);
3030 
3031         if (caps == 2) {
3032             dest.moveTo(h1, bottom);
3033             dest.lineTo(h1 - dist, bottom + dist);
3034             dest.lineTo(h1, bottom);
3035             dest.lineTo(h1 + dist, bottom + dist);
3036         } else if (caps == 1) {
3037             dest.moveTo(h1, bottom);
3038             dest.lineTo(h1 - dist, bottom + dist);
3039 
3040             dest.moveTo(h1 - dist, bottom + dist - 0.5f);
3041             dest.lineTo(h1 + dist, bottom + dist - 0.5f);
3042 
3043             dest.moveTo(h1 + dist, bottom + dist);
3044             dest.lineTo(h1, bottom);
3045         }
3046 
3047         if (fn == 2) {
3048             dest.moveTo(h1, top);
3049             dest.lineTo(h1 - dist, top - dist);
3050             dest.lineTo(h1, top);
3051             dest.lineTo(h1 + dist, top - dist);
3052         } else if (fn == 1) {
3053             dest.moveTo(h1, top);
3054             dest.lineTo(h1 - dist, top - dist);
3055 
3056             dest.moveTo(h1 - dist, top - dist + 0.5f);
3057             dest.lineTo(h1 + dist, top - dist + 0.5f);
3058 
3059             dest.moveTo(h1 + dist, top - dist);
3060             dest.lineTo(h1, top);
3061         }
3062     }
3063 
addSelection(int line, int start, int end, int top, int bottom, SelectionRectangleConsumer consumer)3064     private void addSelection(int line, int start, int end,
3065             int top, int bottom, SelectionRectangleConsumer consumer) {
3066         int linestart = getLineStart(line);
3067         int lineend = getLineEnd(line);
3068         Directions dirs = getLineDirections(line);
3069 
3070         if (lineend > linestart && mText.charAt(lineend - 1) == '\n') {
3071             lineend--;
3072         }
3073 
3074         for (int i = 0; i < dirs.mDirections.length; i += 2) {
3075             int here = linestart + dirs.mDirections[i];
3076             int there = here + (dirs.mDirections[i + 1] & RUN_LENGTH_MASK);
3077 
3078             if (there > lineend) {
3079                 there = lineend;
3080             }
3081 
3082             if (start <= there && end >= here) {
3083                 int st = Math.max(start, here);
3084                 int en = Math.min(end, there);
3085 
3086                 if (st != en) {
3087                     float h1 = getHorizontal(st, false, line, false /* not clamped */);
3088                     float h2 = getHorizontal(en, true, line, false /* not clamped */);
3089 
3090                     float left = Math.min(h1, h2);
3091                     float right = Math.max(h1, h2);
3092 
3093                     final @TextSelectionLayout int layout =
3094                             ((dirs.mDirections[i + 1] & RUN_RTL_FLAG) != 0)
3095                                     ? TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT
3096                                     : TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT;
3097 
3098                     consumer.accept(left, top, right, bottom, layout);
3099                 }
3100             }
3101         }
3102     }
3103 
3104     /**
3105      * Fills in the specified Path with a representation of a highlight
3106      * between the specified offsets.  This will often be a rectangle
3107      * or a potentially discontinuous set of rectangles.  If the start
3108      * and end are the same, the returned path is empty.
3109      */
getSelectionPath(int start, int end, Path dest)3110     public void getSelectionPath(int start, int end, Path dest) {
3111         dest.reset();
3112         getSelection(start, end, (left, top, right, bottom, textSelectionLayout) ->
3113                 dest.addRect(left, top, right, bottom, Path.Direction.CW));
3114     }
3115 
3116     /**
3117      * Calculates the rectangles which should be highlighted to indicate a selection between start
3118      * and end and feeds them into the given {@link SelectionRectangleConsumer}.
3119      *
3120      * @param start    the starting index of the selection
3121      * @param end      the ending index of the selection
3122      * @param consumer the {@link SelectionRectangleConsumer} which will receive the generated
3123      *                 rectangles. It will be called every time a rectangle is generated.
3124      * @hide
3125      * @see #getSelectionPath(int, int, Path)
3126      */
getSelection(int start, int end, final SelectionRectangleConsumer consumer)3127     public final void getSelection(int start, int end, final SelectionRectangleConsumer consumer) {
3128         if (start == end) {
3129             return;
3130         }
3131 
3132         if (end < start) {
3133             int temp = end;
3134             end = start;
3135             start = temp;
3136         }
3137 
3138         final int startline = getLineForOffset(start);
3139         final int endline = getLineForOffset(end);
3140 
3141         int top = getLineTop(startline);
3142         int bottom = getLineBottom(endline, /* includeLineSpacing= */ false);
3143 
3144         if (startline == endline) {
3145             addSelection(startline, start, end, top, bottom, consumer);
3146         } else {
3147             final float width = mWidth;
3148 
3149             addSelection(startline, start, getLineEnd(startline),
3150                     top, getLineBottom(startline), consumer);
3151 
3152             if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) {
3153                 consumer.accept(getLineLeft(startline), top, 0, getLineBottom(startline),
3154                         TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT);
3155             } else {
3156                 consumer.accept(getLineRight(startline), top, width, getLineBottom(startline),
3157                         TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT);
3158             }
3159 
3160             for (int i = startline + 1; i < endline; i++) {
3161                 top = getLineTop(i);
3162                 bottom = getLineBottom(i);
3163                 if (getParagraphDirection(i) == DIR_RIGHT_TO_LEFT) {
3164                     consumer.accept(0, top, width, bottom, TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT);
3165                 } else {
3166                     consumer.accept(0, top, width, bottom, TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT);
3167                 }
3168             }
3169 
3170             top = getLineTop(endline);
3171             bottom = getLineBottom(endline, /* includeLineSpacing= */ false);
3172 
3173             addSelection(endline, getLineStart(endline), end, top, bottom, consumer);
3174 
3175             if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT) {
3176                 consumer.accept(width, top, getLineRight(endline), bottom,
3177                         TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT);
3178             } else {
3179                 consumer.accept(0, top, getLineLeft(endline), bottom,
3180                         TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT);
3181             }
3182         }
3183     }
3184 
3185     /**
3186      * Get the alignment of the specified paragraph, taking into account
3187      * markup attached to it.
3188      */
getParagraphAlignment(int line)3189     public final Alignment getParagraphAlignment(int line) {
3190         Alignment align = mAlignment;
3191 
3192         if (mSpannedText) {
3193             Spanned sp = (Spanned) mText;
3194             AlignmentSpan[] spans = getParagraphSpans(sp, getLineStart(line),
3195                                                 getLineEnd(line),
3196                                                 AlignmentSpan.class);
3197 
3198             int spanLength = spans.length;
3199             if (spanLength > 0) {
3200                 align = spans[spanLength-1].getAlignment();
3201             }
3202         }
3203 
3204         return align;
3205     }
3206 
3207     /**
3208      * Get the left edge of the specified paragraph, inset by left margins.
3209      */
getParagraphLeft(int line)3210     public final int getParagraphLeft(int line) {
3211         int left = 0;
3212         int dir = getParagraphDirection(line);
3213         if (dir == DIR_RIGHT_TO_LEFT || !mSpannedText) {
3214             return left; // leading margin has no impact, or no styles
3215         }
3216         return getParagraphLeadingMargin(line);
3217     }
3218 
3219     /**
3220      * Get the right edge of the specified paragraph, inset by right margins.
3221      */
getParagraphRight(int line)3222     public final int getParagraphRight(int line) {
3223         int right = mWidth;
3224         int dir = getParagraphDirection(line);
3225         if (dir == DIR_LEFT_TO_RIGHT || !mSpannedText) {
3226             return right; // leading margin has no impact, or no styles
3227         }
3228         return right - getParagraphLeadingMargin(line);
3229     }
3230 
3231     /**
3232      * Returns the effective leading margin (unsigned) for this line,
3233      * taking into account LeadingMarginSpan and LeadingMarginSpan2.
3234      * @param line the line index
3235      * @return the leading margin of this line
3236      */
getParagraphLeadingMargin(int line)3237     private int getParagraphLeadingMargin(int line) {
3238         if (!mSpannedText) {
3239             return 0;
3240         }
3241         Spanned spanned = (Spanned) mText;
3242 
3243         int lineStart = getLineStart(line);
3244         int lineEnd = getLineEnd(line);
3245         int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd,
3246                 LeadingMarginSpan.class);
3247         LeadingMarginSpan[] spans = getParagraphSpans(spanned, lineStart, spanEnd,
3248                                                 LeadingMarginSpan.class);
3249         if (spans.length == 0) {
3250             return 0; // no leading margin span;
3251         }
3252 
3253         int margin = 0;
3254 
3255         boolean useFirstLineMargin = lineStart == 0 || spanned.charAt(lineStart - 1) == '\n';
3256         for (int i = 0; i < spans.length; i++) {
3257             if (spans[i] instanceof LeadingMarginSpan2) {
3258                 int spStart = spanned.getSpanStart(spans[i]);
3259                 int spanLine = getLineForOffset(spStart);
3260                 int count = ((LeadingMarginSpan2) spans[i]).getLeadingMarginLineCount();
3261                 // if there is more than one LeadingMarginSpan2, use the count that is greatest
3262                 useFirstLineMargin |= line < spanLine + count;
3263             }
3264         }
3265         for (int i = 0; i < spans.length; i++) {
3266             LeadingMarginSpan span = spans[i];
3267             margin += span.getLeadingMargin(useFirstLineMargin);
3268         }
3269 
3270         return margin;
3271     }
3272 
3273     private static float measurePara(TextPaint paint, CharSequence text, int start, int end,
3274             TextDirectionHeuristic textDir, boolean useBoundsForWidth) {
3275         MeasuredParagraph mt = null;
3276         TextLine tl = TextLine.obtain();
3277         try {
3278             mt = MeasuredParagraph.buildForBidi(text, start, end, textDir, mt);
3279             final char[] chars = mt.getChars();
3280             final int len = chars.length;
3281             final Directions directions = mt.getDirections(0, len);
3282             final int dir = mt.getParagraphDir();
3283             boolean hasTabs = false;
3284             TabStops tabStops = null;
3285             // leading margins should be taken into account when measuring a paragraph
3286             int margin = 0;
3287             if (text instanceof Spanned) {
3288                 Spanned spanned = (Spanned) text;
3289                 LeadingMarginSpan[] spans = getParagraphSpans(spanned, start, end,
3290                         LeadingMarginSpan.class);
3291                 for (LeadingMarginSpan lms : spans) {
3292                     margin += lms.getLeadingMargin(true);
3293                 }
3294             }
3295             for (int i = 0; i < len; ++i) {
3296                 if (chars[i] == '\t') {
3297                     hasTabs = true;
3298                     if (text instanceof Spanned) {
3299                         Spanned spanned = (Spanned) text;
3300                         int spanEnd = spanned.nextSpanTransition(start, end,
3301                                 TabStopSpan.class);
3302                         TabStopSpan[] spans = getParagraphSpans(spanned, start, spanEnd,
3303                                 TabStopSpan.class);
3304                         if (spans.length > 0) {
3305                             tabStops = new TabStops(TAB_INCREMENT, spans);
3306                         }
3307                     }
3308                     break;
3309                 }
3310             }
3311             tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops,
3312                     0 /* ellipsisStart */, 0 /* ellipsisEnd */,
3313                     false /* use fallback line spacing. unused */);
3314             return margin + Math.abs(tl.metrics(null, null, useBoundsForWidth, null));
3315         } finally {
3316             TextLine.recycle(tl);
3317             if (mt != null) {
3318                 mt.recycle();
3319             }
3320         }
3321     }
3322 
3323     /**
3324      * @hide
3325      */
3326     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
3327     public static class TabStops {
3328         private float[] mStops;
3329         private int mNumStops;
3330         private float mIncrement;
3331 
3332         public TabStops(float increment, Object[] spans) {
3333             reset(increment, spans);
3334         }
3335 
3336         void reset(float increment, Object[] spans) {
3337             this.mIncrement = increment;
3338 
3339             int ns = 0;
3340             if (spans != null) {
3341                 float[] stops = this.mStops;
3342                 for (Object o : spans) {
3343                     if (o instanceof TabStopSpan) {
3344                         if (stops == null) {
3345                             stops = new float[10];
3346                         } else if (ns == stops.length) {
3347                             float[] nstops = new float[ns * 2];
3348                             for (int i = 0; i < ns; ++i) {
3349                                 nstops[i] = stops[i];
3350                             }
3351                             stops = nstops;
3352                         }
3353                         stops[ns++] = ((TabStopSpan) o).getTabStop();
3354                     }
3355                 }
3356                 if (ns > 1) {
3357                     Arrays.sort(stops, 0, ns);
3358                 }
3359                 if (stops != this.mStops) {
3360                     this.mStops = stops;
3361                 }
3362             }
3363             this.mNumStops = ns;
3364         }
3365 
3366         float nextTab(float h) {
3367             int ns = this.mNumStops;
3368             if (ns > 0) {
3369                 float[] stops = this.mStops;
3370                 for (int i = 0; i < ns; ++i) {
3371                     float stop = stops[i];
3372                     if (stop > h) {
3373                         return stop;
3374                     }
3375                 }
3376             }
3377             return nextDefaultStop(h, mIncrement);
3378         }
3379 
3380         /**
3381          * Returns the position of next tab stop.
3382          */
3383         public static float nextDefaultStop(float h, float inc) {
3384             return ((int) ((h + inc) / inc)) * inc;
3385         }
3386     }
3387 
3388     /**
3389      * Returns the position of the next tab stop after h on the line.
3390      *
3391      * @param text the text
3392      * @param start start of the line
3393      * @param end limit of the line
3394      * @param h the current horizontal offset
3395      * @param tabs the tabs, can be null.  If it is null, any tabs in effect
3396      * on the line will be used.  If there are no tabs, a default offset
3397      * will be used to compute the tab stop.
3398      * @return the offset of the next tab stop.
3399      */
3400     /* package */ static float nextTab(CharSequence text, int start, int end,
3401                                        float h, Object[] tabs) {
3402         float nh = Float.MAX_VALUE;
3403         boolean alltabs = false;
3404 
3405         if (text instanceof Spanned) {
3406             if (tabs == null) {
3407                 tabs = getParagraphSpans((Spanned) text, start, end, TabStopSpan.class);
3408                 alltabs = true;
3409             }
3410 
3411             for (int i = 0; i < tabs.length; i++) {
3412                 if (!alltabs) {
3413                     if (!(tabs[i] instanceof TabStopSpan))
3414                         continue;
3415                 }
3416 
3417                 int where = ((TabStopSpan) tabs[i]).getTabStop();
3418 
3419                 if (where < nh && where > h)
3420                     nh = where;
3421             }
3422 
3423             if (nh != Float.MAX_VALUE)
3424                 return nh;
3425         }
3426 
3427         return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT;
3428     }
3429 
3430     protected final boolean isSpanned() {
3431         return mSpannedText;
3432     }
3433 
3434     /**
3435      * Returns the same as <code>text.getSpans()</code>, except where
3436      * <code>start</code> and <code>end</code> are the same and are not
3437      * at the very beginning of the text, in which case an empty array
3438      * is returned instead.
3439      * <p>
3440      * This is needed because of the special case that <code>getSpans()</code>
3441      * on an empty range returns the spans adjacent to that range, which is
3442      * primarily for the sake of <code>TextWatchers</code> so they will get
3443      * notifications when text goes from empty to non-empty.  But it also
3444      * has the unfortunate side effect that if the text ends with an empty
3445      * paragraph, that paragraph accidentally picks up the styles of the
3446      * preceding paragraph (even though those styles will not be picked up
3447      * by new text that is inserted into the empty paragraph).
3448      * <p>
3449      * The reason it just checks whether <code>start</code> and <code>end</code>
3450      * is the same is that the only time a line can contain 0 characters
3451      * is if it is the final paragraph of the Layout; otherwise any line will
3452      * contain at least one printing or newline character.  The reason for the
3453      * additional check if <code>start</code> is greater than 0 is that
3454      * if the empty paragraph is the entire content of the buffer, paragraph
3455      * styles that are already applied to the buffer will apply to text that
3456      * is inserted into it.
3457      */
3458     /* package */static <T> T[] getParagraphSpans(Spanned text, int start, int end, Class<T> type) {
3459         if (start == end && start > 0) {
3460             return ArrayUtils.emptyArray(type);
3461         }
3462 
3463         if(text instanceof SpannableStringBuilder) {
3464             return ((SpannableStringBuilder) text).getSpans(start, end, type, false);
3465         } else {
3466             return text.getSpans(start, end, type);
3467         }
3468     }
3469 
3470     private void ellipsize(int start, int end, int line,
3471                            char[] dest, int destoff, TextUtils.TruncateAt method) {
3472         final int ellipsisCount = getEllipsisCount(line);
3473         if (ellipsisCount == 0) {
3474             return;
3475         }
3476         final int ellipsisStart = getEllipsisStart(line);
3477         final int lineStart = getLineStart(line);
3478 
3479         final String ellipsisString = TextUtils.getEllipsisString(method);
3480         final int ellipsisStringLen = ellipsisString.length();
3481         // Use the ellipsis string only if there are that at least as many characters to replace.
3482         final boolean useEllipsisString = ellipsisCount >= ellipsisStringLen;
3483         final int min = Math.max(0, start - ellipsisStart - lineStart);
3484         final int max = Math.min(ellipsisCount, end - ellipsisStart - lineStart);
3485 
3486         for (int i = min; i < max; i++) {
3487             final char c;
3488             if (useEllipsisString && i < ellipsisStringLen) {
3489                 c = ellipsisString.charAt(i);
3490             } else {
3491                 c = TextUtils.ELLIPSIS_FILLER;
3492             }
3493 
3494             final int a = i + ellipsisStart + lineStart;
3495             dest[destoff + a - start] = c;
3496         }
3497     }
3498 
3499     /**
3500      * Stores information about bidirectional (left-to-right or right-to-left)
3501      * text within the layout of a line.
3502      */
3503     public static class Directions {
3504         /**
3505          * Directions represents directional runs within a line of text. Runs are pairs of ints
3506          * listed in visual order, starting from the leading margin.  The first int of each pair is
3507          * the offset from the first character of the line to the start of the run.  The second int
3508          * represents both the length and level of the run. The length is in the lower bits,
3509          * accessed by masking with RUN_LENGTH_MASK.  The level is in the higher bits, accessed by
3510          * shifting by RUN_LEVEL_SHIFT and masking by RUN_LEVEL_MASK. To simply test for an RTL
3511          * direction, test the bit using RUN_RTL_FLAG, if set then the direction is rtl.
3512          * @hide
3513          */
3514         @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
3515         public int[] mDirections;
3516 
3517         /**
3518          * @hide
3519          */
3520         @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
Directions(int[] dirs)3521         public Directions(int[] dirs) {
3522             mDirections = dirs;
3523         }
3524 
3525         /**
3526          * Returns number of BiDi runs.
3527          *
3528          * @hide
3529          */
getRunCount()3530         public @IntRange(from = 0) int getRunCount() {
3531             return mDirections.length / 2;
3532         }
3533 
3534         /**
3535          * Returns the start offset of the BiDi run.
3536          *
3537          * @param runIndex the index of the BiDi run
3538          * @return the start offset of the BiDi run.
3539          * @hide
3540          */
getRunStart(@ntRangefrom = 0) int runIndex)3541         public @IntRange(from = 0) int getRunStart(@IntRange(from = 0) int runIndex) {
3542             return mDirections[runIndex * 2];
3543         }
3544 
3545         /**
3546          * Returns the length of the BiDi run.
3547          *
3548          * Note that this method may return too large number due to reducing the number of object
3549          * allocations. The too large number means the remaining part is assigned to this run. The
3550          * caller must clamp the returned value.
3551          *
3552          * @param runIndex the index of the BiDi run
3553          * @return the length of the BiDi run.
3554          * @hide
3555          */
getRunLength(@ntRangefrom = 0) int runIndex)3556         public @IntRange(from = 0) int getRunLength(@IntRange(from = 0) int runIndex) {
3557             return mDirections[runIndex * 2 + 1] & RUN_LENGTH_MASK;
3558         }
3559 
3560         /**
3561          * Returns the BiDi level of this run.
3562          *
3563          * @param runIndex the index of the BiDi run
3564          * @return the BiDi level of this run.
3565          * @hide
3566          */
3567         @IntRange(from = 0)
getRunLevel(int runIndex)3568         public int getRunLevel(int runIndex) {
3569             return (mDirections[runIndex * 2 + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
3570         }
3571 
3572         /**
3573          * Returns true if the BiDi run is RTL.
3574          *
3575          * @param runIndex the index of the BiDi run
3576          * @return true if the BiDi run is RTL.
3577          * @hide
3578          */
isRunRtl(int runIndex)3579         public boolean isRunRtl(int runIndex) {
3580             return (mDirections[runIndex * 2 + 1] & RUN_RTL_FLAG) != 0;
3581         }
3582     }
3583 
3584     /**
3585      * Return the offset of the first character to be ellipsized away,
3586      * relative to the start of the line.  (So 0 if the beginning of the
3587      * line is ellipsized, not getLineStart().)
3588      */
3589     public abstract int getEllipsisStart(int line);
3590 
3591     /**
3592      * Returns the number of characters to be ellipsized away, or 0 if
3593      * no ellipsis is to take place.
3594      */
3595     public abstract int getEllipsisCount(int line);
3596 
3597     /* package */ static class Ellipsizer implements CharSequence, GetChars {
3598         /* package */ CharSequence mText;
3599         /* package */ Layout mLayout;
3600         /* package */ int mWidth;
3601         /* package */ TextUtils.TruncateAt mMethod;
3602 
Ellipsizer(CharSequence s)3603         public Ellipsizer(CharSequence s) {
3604             mText = s;
3605         }
3606 
charAt(int off)3607         public char charAt(int off) {
3608             char[] buf = TextUtils.obtain(1);
3609             getChars(off, off + 1, buf, 0);
3610             char ret = buf[0];
3611 
3612             TextUtils.recycle(buf);
3613             return ret;
3614         }
3615 
getChars(int start, int end, char[] dest, int destoff)3616         public void getChars(int start, int end, char[] dest, int destoff) {
3617             int line1 = mLayout.getLineForOffset(start);
3618             int line2 = mLayout.getLineForOffset(end);
3619 
3620             TextUtils.getChars(mText, start, end, dest, destoff);
3621 
3622             for (int i = line1; i <= line2; i++) {
3623                 mLayout.ellipsize(start, end, i, dest, destoff, mMethod);
3624             }
3625         }
3626 
length()3627         public int length() {
3628             return mText.length();
3629         }
3630 
subSequence(int start, int end)3631         public CharSequence subSequence(int start, int end) {
3632             char[] s = new char[end - start];
3633             getChars(start, end, s, 0);
3634             return new String(s);
3635         }
3636 
3637         @Override
toString()3638         public String toString() {
3639             char[] s = new char[length()];
3640             getChars(0, length(), s, 0);
3641             return new String(s);
3642         }
3643 
3644     }
3645 
3646     /* package */ static class SpannedEllipsizer extends Ellipsizer implements Spanned {
3647         private Spanned mSpanned;
3648 
SpannedEllipsizer(CharSequence display)3649         public SpannedEllipsizer(CharSequence display) {
3650             super(display);
3651             mSpanned = (Spanned) display;
3652         }
3653 
getSpans(int start, int end, Class<T> type)3654         public <T> T[] getSpans(int start, int end, Class<T> type) {
3655             return mSpanned.getSpans(start, end, type);
3656         }
3657 
getSpanStart(Object tag)3658         public int getSpanStart(Object tag) {
3659             return mSpanned.getSpanStart(tag);
3660         }
3661 
getSpanEnd(Object tag)3662         public int getSpanEnd(Object tag) {
3663             return mSpanned.getSpanEnd(tag);
3664         }
3665 
getSpanFlags(Object tag)3666         public int getSpanFlags(Object tag) {
3667             return mSpanned.getSpanFlags(tag);
3668         }
3669 
3670         @SuppressWarnings("rawtypes")
nextSpanTransition(int start, int limit, Class type)3671         public int nextSpanTransition(int start, int limit, Class type) {
3672             return mSpanned.nextSpanTransition(start, limit, type);
3673         }
3674 
3675         @Override
subSequence(int start, int end)3676         public CharSequence subSequence(int start, int end) {
3677             char[] s = new char[end - start];
3678             getChars(start, end, s, 0);
3679 
3680             SpannableString ss = new SpannableString(new String(s));
3681             TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0);
3682             return ss;
3683         }
3684     }
3685 
3686     private CharSequence mText;
3687     @UnsupportedAppUsage
3688     private TextPaint mPaint;
3689     private final TextPaint mWorkPaint = new TextPaint();
3690     private final Paint mWorkPlainPaint = new Paint();
3691     private int mWidth;
3692     private Alignment mAlignment = Alignment.ALIGN_NORMAL;
3693     private float mSpacingMult;
3694     private float mSpacingAdd;
3695     private static final Rect sTempRect = new Rect();
3696     private boolean mSpannedText;
3697     @Nullable private SpanColors mSpanColors;
3698     private TextDirectionHeuristic mTextDir;
3699     private SpanSet<LineBackgroundSpan> mLineBackgroundSpans;
3700     private boolean mIncludePad;
3701     private boolean mFallbackLineSpacing;
3702     private int mEllipsizedWidth;
3703     private TextUtils.TruncateAt mEllipsize;
3704     private int mMaxLines;
3705     private int mBreakStrategy;
3706     private int mHyphenationFrequency;
3707     private int[] mLeftIndents;
3708     private int[] mRightIndents;
3709     private int mJustificationMode;
3710     private LineBreakConfig mLineBreakConfig;
3711     private boolean mUseBoundsForWidth;
3712     private boolean mShiftDrawingOffsetForStartOverhang;
3713     private @Nullable Paint.FontMetrics mMinimumFontMetrics;
3714 
3715     private TextLine.LineInfo mLineInfo = null;
3716 
3717     /** @hide */
3718     @IntDef(prefix = { "DIR_" }, value = {
3719             DIR_LEFT_TO_RIGHT,
3720             DIR_RIGHT_TO_LEFT
3721     })
3722     @Retention(RetentionPolicy.SOURCE)
3723     public @interface Direction {}
3724 
3725     public static final int DIR_LEFT_TO_RIGHT = 1;
3726     public static final int DIR_RIGHT_TO_LEFT = -1;
3727 
3728     /* package */ static final int DIR_REQUEST_LTR = 1;
3729     /* package */ static final int DIR_REQUEST_RTL = -1;
3730     @UnsupportedAppUsage
3731     /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2;
3732     /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2;
3733 
3734     /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff;
3735     /* package */ static final int RUN_LEVEL_SHIFT = 26;
3736     /* package */ static final int RUN_LEVEL_MASK = 0x3f;
3737     /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT;
3738 
3739     public enum Alignment {
3740         ALIGN_NORMAL,
3741         ALIGN_OPPOSITE,
3742         ALIGN_CENTER,
3743         /** @hide */
3744         @UnsupportedAppUsage
3745         ALIGN_LEFT,
3746         /** @hide */
3747         @UnsupportedAppUsage
3748         ALIGN_RIGHT,
3749     }
3750 
3751     private static final float TAB_INCREMENT = 20;
3752 
3753     /** @hide */
3754     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
3755     @UnsupportedAppUsage
3756     public static final Directions DIRS_ALL_LEFT_TO_RIGHT =
3757         new Directions(new int[] { 0, RUN_LENGTH_MASK });
3758 
3759     /** @hide */
3760     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
3761     @UnsupportedAppUsage
3762     public static final Directions DIRS_ALL_RIGHT_TO_LEFT =
3763         new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG });
3764 
3765     /** @hide */
3766     @Retention(RetentionPolicy.SOURCE)
3767     @IntDef(prefix = { "TEXT_SELECTION_LAYOUT_" }, value = {
3768             TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT,
3769             TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT
3770     })
3771     public @interface TextSelectionLayout {}
3772 
3773     /** @hide */
3774     public static final int TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT = 0;
3775     /** @hide */
3776     public static final int TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT = 1;
3777 
3778     /** @hide */
3779     @FunctionalInterface
3780     public interface SelectionRectangleConsumer {
3781         /**
3782          * Performs this operation on the given rectangle.
3783          *
3784          * @param left   the left edge of the rectangle
3785          * @param top    the top edge of the rectangle
3786          * @param right  the right edge of the rectangle
3787          * @param bottom the bottom edge of the rectangle
3788          * @param textSelectionLayout the layout (RTL or LTR) of the text covered by this
3789          *                            selection rectangle
3790          */
3791         void accept(float left, float top, float right, float bottom,
3792                 @TextSelectionLayout int textSelectionLayout);
3793     }
3794 
3795     /**
3796      * Strategy for determining whether a text segment is inside a rectangle area.
3797      *
3798      * @see #getRangeForRect(RectF, SegmentFinder, TextInclusionStrategy)
3799      */
3800     @FunctionalInterface
3801     public interface TextInclusionStrategy {
3802         /**
3803          * Returns true if this {@link TextInclusionStrategy} considers the segment with bounds
3804          * {@code segmentBounds} to be inside {@code area}.
3805          *
3806          * <p>The segment is a range of text which does not cross line boundaries or directional run
3807          * boundaries. The horizontal bounds of the segment are the start bound of the first
3808          * character to the end bound of the last character. The vertical bounds match the line
3809          * bounds ({@code getLineTop(line)} and {@code getLineBottom(line, false)}).
3810          */
3811         boolean isSegmentInside(@NonNull RectF segmentBounds, @NonNull RectF area);
3812     }
3813 
3814     /**
3815      * A builder class for Layout object.
3816      *
3817      * Different from {@link StaticLayout.Builder}, this builder generates the optimal layout based
3818      * on input. If the given text and parameters can be rendered with {@link BoringLayout}, this
3819      * builder generates {@link BoringLayout} instance. Otherwise, {@link StaticLayout} instance is
3820      * generated.
3821      *
3822      * @see StaticLayout.Builder
3823      */
3824     @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
3825     public static final class Builder {
3826         /**
3827          * Construct a builder class.
3828          *
3829          * @param text a text to be displayed.
3830          * @param start an inclusive start index of the text to be displayed.
3831          * @param end an exclusive end index of the text to be displayed.
3832          * @param paint a paint object to be used for drawing text.
3833          * @param width a width constraint in pixels.
3834          */
Builder( @onNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextPaint paint, @IntRange(from = 0) int width)3835         public Builder(
3836                 @NonNull CharSequence text,
3837                 @IntRange(from = 0) int start,
3838                 @IntRange(from = 0) int end,
3839                 @NonNull TextPaint paint,
3840                 @IntRange(from = 0) int width) {
3841             mText = text;
3842             mStart = start;
3843             mEnd = end;
3844             mPaint = paint;
3845             mWidth = width;
3846             mEllipsizedWidth = width;
3847         }
3848 
3849         /**
3850          * Set the text alignment.
3851          *
3852          * The default value is {@link Layout.Alignment#ALIGN_NORMAL}.
3853          *
3854          * @param alignment an alignment.
3855          * @return this builder instance.
3856          * @see Layout.Alignment
3857          * @see Layout#getAlignment()
3858          * @see StaticLayout.Builder#setAlignment(Alignment)
3859          */
3860         @NonNull
setAlignment(@onNull Alignment alignment)3861         public Builder setAlignment(@NonNull Alignment alignment) {
3862             mAlignment = alignment;
3863             return this;
3864         }
3865 
3866         /**
3867          * Set the text direction heuristics.
3868          *
3869          * The text direction heuristics is used to resolve text direction on the text.
3870          *
3871          * The default value is {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}
3872          *
3873          * @param textDirection a text direction heuristic.
3874          * @return this builder instance.
3875          * @see TextDirectionHeuristics
3876          * @see Layout#getTextDirectionHeuristic()
3877          * @see StaticLayout.Builder#setTextDirection(TextDirectionHeuristic)
3878          */
3879         @NonNull
setTextDirectionHeuristic(@onNull TextDirectionHeuristic textDirection)3880         public Builder setTextDirectionHeuristic(@NonNull TextDirectionHeuristic textDirection) {
3881             mTextDir = textDirection;
3882             return this;
3883         }
3884 
3885         /**
3886          * Set the line spacing amount.
3887          *
3888          * The specified amount of pixels will be added to each line.
3889          *
3890          * The default value is {@code 0}. The negative value is allowed for squeezing lines.
3891          *
3892          * @param amount an amount of pixels to be added to line height.
3893          * @return this builder instance.
3894          * @see Layout#getLineSpacingAmount()
3895          * @see Layout#getSpacingAdd()
3896          * @see StaticLayout.Builder#setLineSpacing(float, float)
3897          */
3898         @NonNull
setLineSpacingAmount(float amount)3899         public Builder setLineSpacingAmount(float amount) {
3900             mSpacingAdd = amount;
3901             return this;
3902         }
3903 
3904         /**
3905          * Set the line spacing multiplier.
3906          *
3907          * The specified value will be multiplied to each line.
3908          *
3909          * The default value is {@code 1}.
3910          *
3911          * @param multiplier a multiplier to be applied to the line height
3912          * @return this builder instance.
3913          * @see Layout#getLineSpacingMultiplier()
3914          * @see Layout#getSpacingMultiplier()
3915          * @see StaticLayout.Builder#setLineSpacing(float, float)
3916          */
3917         @NonNull
setLineSpacingMultiplier(@loatRangefrom = 0) float multiplier)3918         public Builder setLineSpacingMultiplier(@FloatRange(from = 0) float multiplier) {
3919             mSpacingMult = multiplier;
3920             return this;
3921         }
3922 
3923         /**
3924          * Set whether including extra padding into the first and the last line height.
3925          *
3926          * By setting true, the first line of the text and the last line of the text will have extra
3927          * vertical space for avoiding clipping.
3928          *
3929          * The default value is {@code true}.
3930          *
3931          * @param includeFontPadding true for including extra space into first and last line.
3932          * @return this builder instance.
3933          * @see Layout#isFontPaddingIncluded()
3934          * @see StaticLayout.Builder#setIncludePad(boolean)
3935          */
3936         @NonNull
setFontPaddingIncluded(boolean includeFontPadding)3937         public Builder setFontPaddingIncluded(boolean includeFontPadding) {
3938             mIncludePad = includeFontPadding;
3939             return this;
3940         }
3941 
3942         /**
3943          * Set whether to respect the ascent and descent of the fallback fonts.
3944          *
3945          * Set whether to respect the ascent and descent of the fallback fonts that are used in
3946          * displaying the text (which is needed to avoid text from consecutive lines running into
3947          * each other). If set, fallback fonts that end up getting used can increase the ascent
3948          * and descent of the lines that they are used on.
3949          *
3950          * The default value is {@code false}
3951          *
3952          * @param fallbackLineSpacing whether to expand line height based on fallback fonts.
3953          * @return this builder instance.
3954          * @see Layout#isFallbackLineSpacingEnabled()
3955          * @see StaticLayout.Builder#setUseLineSpacingFromFallbacks(boolean)
3956          */
3957         @NonNull
setFallbackLineSpacingEnabled(boolean fallbackLineSpacing)3958         public Builder setFallbackLineSpacingEnabled(boolean fallbackLineSpacing) {
3959             mFallbackLineSpacing = fallbackLineSpacing;
3960             return this;
3961         }
3962 
3963         /**
3964          * Set the width as used for ellipsizing purpose in pixels.
3965          *
3966          * The passed value is ignored and forced to set to the value of width constraint passed in
3967          * constructor if no ellipsize option is set.
3968          *
3969          * The default value is the width constraint.
3970          *
3971          * @param ellipsizeWidth a ellipsizing width in pixels.
3972          * @return this builder instance.
3973          * @see Layout#getEllipsizedWidth()
3974          * @see StaticLayout.Builder#setEllipsizedWidth(int)
3975          */
3976         @NonNull
setEllipsizedWidth(@ntRangefrom = 0) int ellipsizeWidth)3977         public Builder setEllipsizedWidth(@IntRange(from = 0) int ellipsizeWidth) {
3978             mEllipsizedWidth = ellipsizeWidth;
3979             return this;
3980         }
3981 
3982         /**
3983          * Set the ellipsizing type.
3984          *
3985          * By setting null, the ellipsize is disabled.
3986          *
3987          * The default value is {@code null}.
3988          *
3989          * @param ellipsize type of the ellipsize. null for disabling ellipsize.
3990          * @return this builder instance.
3991          * @see Layout#getEllipsize()
3992          * @see StaticLayout.Builder#getEllipsize()
3993          * @see android.text.TextUtils.TruncateAt
3994          */
3995         @NonNull
setEllipsize(@ullable TextUtils.TruncateAt ellipsize)3996         public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) {
3997             mEllipsize = ellipsize;
3998             return this;
3999         }
4000 
4001         /**
4002          * Set the maximum number of lines.
4003          *
4004          * The default value is unlimited.
4005          *
4006          * @param maxLines maximum number of lines in the layout.
4007          * @return this builder instance.
4008          * @see Layout#getMaxLines()
4009          * @see StaticLayout.Builder#setMaxLines(int)
4010          */
4011         @NonNull
setMaxLines(@ntRangefrom = 1) int maxLines)4012         public Builder setMaxLines(@IntRange(from = 1) int maxLines) {
4013             mMaxLines = maxLines;
4014             return this;
4015         }
4016 
4017         /**
4018          * Set the line break strategy.
4019          *
4020          * The default value is {@link Layout#BREAK_STRATEGY_SIMPLE}.
4021          *
4022          * @param breakStrategy a break strategy for line breaking.
4023          * @return this builder instance.
4024          * @see Layout#getBreakStrategy()
4025          * @see StaticLayout.Builder#setBreakStrategy(int)
4026          * @see Layout#BREAK_STRATEGY_SIMPLE
4027          * @see Layout#BREAK_STRATEGY_HIGH_QUALITY
4028          * @see Layout#BREAK_STRATEGY_BALANCED
4029          */
4030         @NonNull
setBreakStrategy(@reakStrategy int breakStrategy)4031         public Builder setBreakStrategy(@BreakStrategy int breakStrategy) {
4032             mBreakStrategy = breakStrategy;
4033             return this;
4034         }
4035 
4036         /**
4037          * Set the hyphenation frequency.
4038          *
4039          * The default value is {@link Layout#HYPHENATION_FREQUENCY_NONE}.
4040          *
4041          * @param hyphenationFrequency a hyphenation frequency.
4042          * @return this builder instance.
4043          * @see Layout#getHyphenationFrequency()
4044          * @see StaticLayout.Builder#setHyphenationFrequency(int)
4045          * @see Layout#HYPHENATION_FREQUENCY_NONE
4046          * @see Layout#HYPHENATION_FREQUENCY_NORMAL
4047          * @see Layout#HYPHENATION_FREQUENCY_FULL
4048          * @see Layout#HYPHENATION_FREQUENCY_NORMAL_FAST
4049          * @see Layout#HYPHENATION_FREQUENCY_FULL_FAST
4050          */
4051         @NonNull
setHyphenationFrequency(@yphenationFrequency int hyphenationFrequency)4052         public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) {
4053             mHyphenationFrequency = hyphenationFrequency;
4054             return this;
4055         }
4056 
4057         /**
4058          * Set visually left indents in pixels per lines.
4059          *
4060          * For the lines past the last element in the array, the last element repeats. Passing null
4061          * for disabling indents.
4062          *
4063          * Note that even with the RTL layout, this method reserve spacing at the visually left of
4064          * the line.
4065          *
4066          * The default value is {@code null}.
4067          *
4068          * @param leftIndents array of indents values for the left margins in pixels.
4069          * @return this builder instance.
4070          * @see Layout#getLeftIndents()
4071          * @see Layout#getRightIndents()
4072          * @see Layout.Builder#setRightIndents(int[])
4073          * @see StaticLayout.Builder#setIndents(int[], int[])
4074          */
4075         @NonNull
setLeftIndents(@ullable int[] leftIndents)4076         public Builder setLeftIndents(@Nullable int[] leftIndents) {
4077             mLeftIndents = leftIndents;
4078             return this;
4079         }
4080 
4081         /**
4082          * Set visually right indents in pixels per lines.
4083          *
4084          * For the lines past the last element in the array, the last element repeats. Passing null
4085          * for disabling indents.
4086          *
4087          * Note that even with the RTL layout, this method reserve spacing at the visually right of
4088          * the line.
4089          *
4090          * The default value is {@code null}.
4091          *
4092          * @param rightIndents array of indents values for the right margins in pixels.
4093          * @return this builder instance.
4094          * @see Layout#getLeftIndents()
4095          * @see Layout#getRightIndents()
4096          * @see Layout.Builder#setLeftIndents(int[])
4097          * @see StaticLayout.Builder#setIndents(int[], int[])
4098          */
4099         @NonNull
setRightIndents(@ullable int[] rightIndents)4100         public Builder setRightIndents(@Nullable int[] rightIndents) {
4101             mRightIndents = rightIndents;
4102             return this;
4103         }
4104 
4105         /**
4106          * Set justification mode.
4107          *
4108          * When justification mode is {@link Layout#JUSTIFICATION_MODE_INTER_WORD}, the word spacing
4109          * on the given Paint passed to the constructor will be ignored. This behavior also affects
4110          * spans which change the word spacing.
4111          *
4112          * The default value is {@link Layout#JUSTIFICATION_MODE_NONE}.
4113          *
4114          * @param justificationMode justification mode.
4115          * @return this builder instance.
4116          * @see Layout#getJustificationMode()
4117          * @see StaticLayout.Builder#setJustificationMode(int)
4118          * @see Layout#JUSTIFICATION_MODE_NONE
4119          * @see Layout#JUSTIFICATION_MODE_INTER_WORD
4120          */
4121         @NonNull
setJustificationMode(@ustificationMode int justificationMode)4122         public Builder setJustificationMode(@JustificationMode int justificationMode) {
4123             mJustificationMode = justificationMode;
4124             return this;
4125         }
4126 
4127         /**
4128          * Set the line break configuration.
4129          *
4130          * The default value is a LinebreakConfig instance that has
4131          * {@link LineBreakConfig#LINE_BREAK_STYLE_NONE} and
4132          * {@link LineBreakConfig#LINE_BREAK_WORD_STYLE_NONE}.
4133          *
4134          * @param lineBreakConfig the line break configuration
4135          * @return this builder instance.
4136          * @see Layout#getLineBreakConfig()
4137          * @see StaticLayout.Builder#setLineBreakConfig(LineBreakConfig)
4138          */
4139         @NonNull
setLineBreakConfig(@onNull LineBreakConfig lineBreakConfig)4140         public Builder setLineBreakConfig(@NonNull LineBreakConfig lineBreakConfig) {
4141             mLineBreakConfig = lineBreakConfig;
4142             return this;
4143         }
4144 
4145         /**
4146          * Set true for using width of bounding box as a source of automatic line breaking and
4147          * drawing.
4148          *
4149          * If this value is false, the Layout determines the drawing offset and automatic line
4150          * breaking based on total advances. By setting true, use all joined glyph's bounding boxes
4151          * as a source of text width.
4152          *
4153          * If the font has glyphs that have negative bearing X or its xMax is greater than advance,
4154          * the glyph clipping can happen because the drawing area may be bigger. By setting this to
4155          * true, the Layout will reserve more spaces for drawing.
4156          *
4157          * @param useBoundsForWidth True for using bounding box, false for advances.
4158          * @return this builder instance
4159          * @see Layout#getUseBoundsForWidth()
4160          * @see StaticLayout.Builder#setUseBoundsForWidth(boolean)
4161          */
4162         // The corresponding getter is getUseBoundsForWidth
4163         @NonNull
4164         @SuppressLint("MissingGetterMatchingBuilder")
4165         @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
setUseBoundsForWidth(boolean useBoundsForWidth)4166         public Builder setUseBoundsForWidth(boolean useBoundsForWidth) {
4167             mUseBoundsForWidth = useBoundsForWidth;
4168             return this;
4169         }
4170 
4171         /**
4172          * Set true for shifting the drawing x offset for showing overhang at the start position.
4173          *
4174          * This flag is ignored if the {@link #getUseBoundsForWidth()} is false.
4175          *
4176          * If this value is false, the Layout draws text from the zero even if there is a glyph
4177          * stroke in a region where the x coordinate is negative.
4178          *
4179          * If this value is true, the Layout draws text with shifting the x coordinate of the
4180          * drawing bounding box.
4181          *
4182          * This value is false by default.
4183          *
4184          * @param shiftDrawingOffsetForStartOverhang true for shifting the drawing offset for
4185          *                                          showing the stroke that is in the region where
4186          *                                          the x coordinate is negative.
4187          * @see #setUseBoundsForWidth(boolean)
4188          * @see #getUseBoundsForWidth()
4189          */
4190         @NonNull
4191         // The corresponding getter is getShiftDrawingOffsetForStartOverhang()
4192         @SuppressLint("MissingGetterMatchingBuilder")
4193         @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
setShiftDrawingOffsetForStartOverhang( boolean shiftDrawingOffsetForStartOverhang)4194         public Builder setShiftDrawingOffsetForStartOverhang(
4195                 boolean shiftDrawingOffsetForStartOverhang) {
4196             mShiftDrawingOffsetForStartOverhang = shiftDrawingOffsetForStartOverhang;
4197             return this;
4198         }
4199 
4200         /**
4201          * Set the minimum font metrics used for line spacing.
4202          *
4203          * <p>
4204          * {@code null} is the default value. If {@code null} is set or left it as default, the font
4205          * metrics obtained by {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} is used.
4206          *
4207          * <p>
4208          * The minimum meaning here is the minimum value of line spacing: maximum value of
4209          * {@link Paint#ascent()}, minimum value of {@link Paint#descent()}.
4210          *
4211          * <p>
4212          * By setting this value, each line will have minimum line spacing regardless of the text
4213          * rendered. For example, usually Japanese script has larger vertical metrics than Latin
4214          * script. By setting the metrics obtained by
4215          * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} for Japanese or leave it
4216          * {@code null} if the Paint's locale is Japanese, the line spacing for Japanese is reserved
4217          * if the text is an English text. If the vertical metrics of the text is larger than
4218          * Japanese, for example Burmese, the bigger font metrics is used.
4219          *
4220          * @param minimumFontMetrics A minimum font metrics. Passing {@code null} for using the
4221          *                          value obtained by
4222          *                          {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)}
4223          * @see android.widget.TextView#setMinimumFontMetrics(Paint.FontMetrics)
4224          * @see android.widget.TextView#getMinimumFontMetrics()
4225          * @see Layout#getMinimumFontMetrics()
4226          * @see StaticLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics)
4227          * @see DynamicLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics)
4228          */
4229         @NonNull
4230         @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE)
setMinimumFontMetrics(@ullable Paint.FontMetrics minimumFontMetrics)4231         public Builder setMinimumFontMetrics(@Nullable Paint.FontMetrics minimumFontMetrics) {
4232             mMinimumFontMetrics = minimumFontMetrics;
4233             return this;
4234         }
4235 
isBoring()4236         private BoringLayout.Metrics isBoring() {
4237             if (mStart != 0 || mEnd != mText.length()) {  // BoringLayout only support entire text.
4238                 return null;
4239             }
4240             BoringLayout.Metrics metrics = BoringLayout.isBoring(mText, mPaint, mTextDir,
4241                     mFallbackLineSpacing, mMinimumFontMetrics, null);
4242             if (metrics == null) {
4243                 return null;
4244             }
4245             if (metrics.width <= mWidth) {
4246                 return metrics;
4247             }
4248             if (mEllipsize != null) {
4249                 return metrics;
4250             }
4251             return null;
4252         }
4253 
4254         /**
4255          * Build a Layout object.
4256          */
4257         @NonNull
build()4258         public Layout build() {
4259             BoringLayout.Metrics metrics = isBoring();
4260             if (metrics == null) {  // we cannot use BoringLayout, create StaticLayout.
4261                 return StaticLayout.Builder.obtain(mText, mStart, mEnd, mPaint, mWidth)
4262                         .setAlignment(mAlignment)
4263                         .setLineSpacing(mSpacingAdd, mSpacingMult)
4264                         .setTextDirection(mTextDir)
4265                         .setIncludePad(mIncludePad)
4266                         .setUseLineSpacingFromFallbacks(mFallbackLineSpacing)
4267                         .setEllipsizedWidth(mEllipsizedWidth)
4268                         .setEllipsize(mEllipsize)
4269                         .setMaxLines(mMaxLines)
4270                         .setBreakStrategy(mBreakStrategy)
4271                         .setHyphenationFrequency(mHyphenationFrequency)
4272                         .setIndents(mLeftIndents, mRightIndents)
4273                         .setJustificationMode(mJustificationMode)
4274                         .setLineBreakConfig(mLineBreakConfig)
4275                         .setUseBoundsForWidth(mUseBoundsForWidth)
4276                         .setShiftDrawingOffsetForStartOverhang(mShiftDrawingOffsetForStartOverhang)
4277                         .build();
4278             } else {
4279                 return new BoringLayout(
4280                         mText, mPaint, mWidth, mAlignment, mTextDir, mSpacingMult, mSpacingAdd,
4281                         mIncludePad, mFallbackLineSpacing, mEllipsizedWidth, mEllipsize, mMaxLines,
4282                         mBreakStrategy, mHyphenationFrequency, mLeftIndents, mRightIndents,
4283                         mJustificationMode, mLineBreakConfig, metrics, mUseBoundsForWidth,
4284                         mShiftDrawingOffsetForStartOverhang, mMinimumFontMetrics);
4285             }
4286         }
4287 
4288         private final CharSequence mText;
4289         private final int mStart;
4290         private final int mEnd;
4291         private final TextPaint mPaint;
4292         private final int mWidth;
4293         private Alignment mAlignment = Alignment.ALIGN_NORMAL;
4294         private float mSpacingMult = 1.0f;
4295         private float mSpacingAdd = 0.0f;
4296         private TextDirectionHeuristic mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
4297         private boolean mIncludePad = true;
4298         private boolean mFallbackLineSpacing = false;
4299         private int mEllipsizedWidth;
4300         private TextUtils.TruncateAt mEllipsize = null;
4301         private int mMaxLines = Integer.MAX_VALUE;
4302         private int mBreakStrategy = BREAK_STRATEGY_SIMPLE;
4303         private int mHyphenationFrequency = HYPHENATION_FREQUENCY_NONE;
4304         private int[] mLeftIndents = null;
4305         private int[] mRightIndents = null;
4306         private int mJustificationMode = JUSTIFICATION_MODE_NONE;
4307         private LineBreakConfig mLineBreakConfig = LineBreakConfig.NONE;
4308         private boolean mUseBoundsForWidth;
4309         private boolean mShiftDrawingOffsetForStartOverhang;
4310         private Paint.FontMetrics mMinimumFontMetrics;
4311     }
4312 
4313     ///////////////////////////////////////////////////////////////////////////////////////////////
4314     // Getters of parameters that is used for building Layout instance
4315     ///////////////////////////////////////////////////////////////////////////////////////////////
4316 
4317     // TODO(316208691): Revive following removed API docs.
4318     // @see Layout.Builder
4319     /**
4320      * Return the text used for creating this layout.
4321      *
4322      * @return the text used for creating this layout.
4323      */
4324     @NonNull
getText()4325     public final CharSequence getText() {
4326         return mText;
4327     }
4328 
4329     // TODO(316208691): Revive following removed API docs.
4330     // @see Layout.Builder
4331     /**
4332      * Return the paint used for creating this layout.
4333      *
4334      * Do not modify the returned paint object. This paint object will still be used for
4335      * drawing/measuring text.
4336      *
4337      * @return the paint used for creating this layout.
4338      */
4339     @NonNull
getPaint()4340     public final TextPaint getPaint() {
4341         return mPaint;
4342     }
4343 
4344     // TODO(316208691): Revive following removed API docs.
4345     // @see Layout.Builder
4346     /**
4347      * Return the width used for creating this layout in pixels.
4348      *
4349      * @return the width used for creating this layout in pixels.
4350      */
4351     @IntRange(from = 0)
getWidth()4352     public final int getWidth() {
4353         return mWidth;
4354     }
4355 
4356     // TODO(316208691): Revive following removed API docs.
4357     // @see Layout.Builder#setAlignment(Alignment)
4358     /**
4359      * Returns the alignment used for creating this layout in pixels.
4360      *
4361      * @return the alignment used for creating this layout.
4362      * @see StaticLayout.Builder#setAlignment(Alignment)
4363      */
4364     @NonNull
getAlignment()4365     public final Alignment getAlignment() {
4366         return mAlignment;
4367     }
4368 
4369     /**
4370      * Returns the text direction heuristic used for creating this layout.
4371      *
4372      * @return the text direction heuristic used for creating this layout
4373      * @see Layout.Builder#setTextDirectionHeuristic(TextDirectionHeuristic)
4374      * @see StaticLayout.Builder#setTextDirection(TextDirectionHeuristic)
4375      */
4376     @NonNull
4377     @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
getTextDirectionHeuristic()4378     public final TextDirectionHeuristic getTextDirectionHeuristic() {
4379         return mTextDir;
4380     }
4381 
4382     // TODO(316208691): Revive following removed API docs.
4383     // This is an alias of {@link #getLineSpacingMultiplier}.
4384     // @see Layout.Builder#setLineSpacingMultiplier(float)
4385     // @see Layout#getLineSpacingMultiplier()
4386     /**
4387      * Returns the multiplier applied to the line height.
4388      *
4389      * @return the line height multiplier.
4390      * @see StaticLayout.Builder#setLineSpacing(float, float)
4391      */
getSpacingMultiplier()4392     public final float getSpacingMultiplier() {
4393         return getLineSpacingMultiplier();
4394     }
4395 
4396     /**
4397      * Returns the multiplier applied to the line height.
4398      *
4399      * @return the line height multiplier.
4400      * @see Layout.Builder#setLineSpacingMultiplier(float)
4401      * @see StaticLayout.Builder#setLineSpacing(float, float)
4402      * @see Layout#getSpacingMultiplier()
4403      */
4404     @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
getLineSpacingMultiplier()4405     public final float getLineSpacingMultiplier() {
4406         return mSpacingMult;
4407     }
4408 
4409     // TODO(316208691): Revive following removed API docs.
4410     // This is an alias of {@link #getLineSpacingAmount()}.
4411     // @see Layout.Builder#setLineSpacingAmount(float)
4412     // @see Layout#getLineSpacingAmount()
4413     /**
4414      * Returns the amount added to the line height.
4415      *
4416      * @return the line height additional amount.
4417      * @see StaticLayout.Builder#setLineSpacing(float, float)
4418      */
getSpacingAdd()4419     public final float getSpacingAdd() {
4420         return getLineSpacingAmount();
4421     }
4422 
4423     /**
4424      * Returns the amount added to the line height.
4425      *
4426      * @return the line height additional amount.
4427      * @see Layout.Builder#setLineSpacingAmount(float)
4428      * @see StaticLayout.Builder#setLineSpacing(float, float)
4429      * @see Layout#getSpacingAdd()
4430      */
4431     @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
getLineSpacingAmount()4432     public final float getLineSpacingAmount() {
4433         return mSpacingAdd;
4434     }
4435 
4436     /**
4437      * Returns true if this layout is created with increased line height.
4438      *
4439      * @return true if the layout is created with increased line height.
4440      * @see Layout.Builder#setFontPaddingIncluded(boolean)
4441      * @see StaticLayout.Builder#setIncludePad(boolean)
4442      */
4443     @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
isFontPaddingIncluded()4444     public final boolean isFontPaddingIncluded() {
4445         return mIncludePad;
4446     }
4447 
4448     // TODO(316208691): Revive following removed API docs.
4449     // @see Layout.Builder#setFallbackLineSpacingEnabled(boolean)
4450     /**
4451      * Return true if the fallback line space is enabled in this Layout.
4452      *
4453      * @return true if the fallback line space is enabled. Otherwise, returns false.
4454      * @see StaticLayout.Builder#setUseLineSpacingFromFallbacks(boolean)
4455      */
4456     // not being final because of already published API.
isFallbackLineSpacingEnabled()4457     public boolean isFallbackLineSpacingEnabled() {
4458         return mFallbackLineSpacing;
4459     }
4460 
4461     // TODO(316208691): Revive following removed API docs.
4462     // @see Layout.Builder#setEllipsizedWidth(int)
4463     // @see Layout.Builder#setEllipsize(TextUtils.TruncateAt)
4464     // @see Layout#getEllipsize()
4465     /**
4466      * Return the width to which this layout is ellipsized.
4467      *
4468      * If no ellipsize is applied, the same amount of {@link #getWidth} is returned.
4469      *
4470      * @return the amount of ellipsized width in pixels.
4471      * @see StaticLayout.Builder#setEllipsizedWidth(int)
4472      * @see StaticLayout.Builder#setEllipsize(TextUtils.TruncateAt)
4473      */
4474     @IntRange(from = 0)
getEllipsizedWidth()4475     public int getEllipsizedWidth() {  // not being final because of already published API.
4476         return mEllipsizedWidth;
4477     }
4478 
4479     /**
4480      * Return the ellipsize option used for creating this layout.
4481      *
4482      * May return null if no ellipsize option was selected.
4483      *
4484      * @return The ellipsize option used for creating this layout, or null if no ellipsize option
4485      * was selected.
4486      * @see Layout.Builder#setEllipsize(TextUtils.TruncateAt)
4487      * @see StaticLayout.Builder#setEllipsize(TextUtils.TruncateAt)
4488      * @see Layout.Builder#setEllipsizedWidth(int)
4489      * @see StaticLayout.Builder#setEllipsizedWidth(int)
4490      * @see Layout#getEllipsizedWidth()
4491      */
4492     @Nullable
4493     @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
getEllipsize()4494     public final TextUtils.TruncateAt getEllipsize() {
4495         return mEllipsize;
4496     }
4497 
4498     /**
4499      * Return the maximum lines allowed used for creating this layout.
4500      *
4501      * Note that this is not an actual line count of this layout. Use {@link #getLineCount()} for
4502      * getting the actual line count of this layout.
4503      *
4504      * @return the maximum lines allowed used for creating this layout.
4505      * @see Layout.Builder#setMaxLines(int)
4506      * @see StaticLayout.Builder#setMaxLines(int)
4507      */
4508     @IntRange(from = 1)
4509     @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
getMaxLines()4510     public final int getMaxLines() {
4511         return mMaxLines;
4512     }
4513 
4514     /**
4515      * Return the break strategy used for creating this layout.
4516      *
4517      * @return the break strategy used for creating this layout.
4518      * @see Layout.Builder#setBreakStrategy(int)
4519      * @see StaticLayout.Builder#setBreakStrategy(int)
4520      */
4521     @BreakStrategy
4522     @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
getBreakStrategy()4523     public final int getBreakStrategy() {
4524         return mBreakStrategy;
4525     }
4526 
4527     /**
4528      * Return the hyphenation frequency used for creating this layout.
4529      *
4530      * @return the hyphenation frequency used for creating this layout.
4531      * @see Layout.Builder#setHyphenationFrequency(int)
4532      * @see StaticLayout.Builder#setHyphenationFrequency(int)
4533      */
4534     @HyphenationFrequency
4535     @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
getHyphenationFrequency()4536     public final int getHyphenationFrequency() {
4537         return mHyphenationFrequency;
4538     }
4539 
4540     /**
4541      * Return a copy of the left indents used for this layout.
4542      *
4543      * May return null if no left indentation is applied.
4544      *
4545      * @return the array of left indents in pixels.
4546      * @see Layout.Builder#setLeftIndents(int[])
4547      * @see Layout.Builder#setRightIndents(int[])
4548      * @see StaticLayout.Builder#setIndents(int[], int[])
4549      */
4550     @Nullable
4551     @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
getLeftIndents()4552     public final int[] getLeftIndents() {
4553         if (mLeftIndents == null) {
4554             return null;
4555         }
4556         int[] newArray = new int[mLeftIndents.length];
4557         System.arraycopy(mLeftIndents, 0, newArray, 0, newArray.length);
4558         return newArray;
4559     }
4560 
4561     /**
4562      * Return a copy of the right indents used for this layout.
4563      *
4564      * May return null if no right indentation is applied.
4565      *
4566      * @return the array of right indents in pixels.
4567      * @see Layout.Builder#setLeftIndents(int[])
4568      * @see Layout.Builder#setRightIndents(int[])
4569      * @see StaticLayout.Builder#setIndents(int[], int[])
4570      */
4571     @Nullable
4572     @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
getRightIndents()4573     public final int[] getRightIndents() {
4574         if (mRightIndents == null) {
4575             return null;
4576         }
4577         int[] newArray = new int[mRightIndents.length];
4578         System.arraycopy(mRightIndents, 0, newArray, 0, newArray.length);
4579         return newArray;
4580     }
4581 
4582     /**
4583      * Return the justification mode used for creating this layout.
4584      *
4585      * @return the justification mode used for creating this layout.
4586      * @see Layout.Builder#setJustificationMode(int)
4587      * @see StaticLayout.Builder#setJustificationMode(int)
4588      */
4589     @JustificationMode
4590     @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
getJustificationMode()4591     public final int getJustificationMode() {
4592         return mJustificationMode;
4593     }
4594 
4595     /**
4596      * Gets the {@link LineBreakConfig} used for creating this layout.
4597      *
4598      * Do not modify the returned object.
4599      *
4600      * @return The line break config used for creating this layout.
4601      */
4602     // not being final because of subclass has already published API.
4603     @NonNull
4604     @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
getLineBreakConfig()4605     public LineBreakConfig getLineBreakConfig() {
4606         return mLineBreakConfig;
4607     }
4608 
4609     /**
4610      * Returns true if using bounding box as a width, false for using advance as a width.
4611      *
4612      * @return True if using bounding box for width, false if using advance for width.
4613      * @see android.widget.TextView#setUseBoundsForWidth(boolean)
4614      * @see android.widget.TextView#getUseBoundsForWidth()
4615      * @see StaticLayout.Builder#setUseBoundsForWidth(boolean)
4616      * @see DynamicLayout.Builder#setUseBoundsForWidth(boolean)
4617      */
4618     @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
getUseBoundsForWidth()4619     public boolean getUseBoundsForWidth() {
4620         return mUseBoundsForWidth;
4621     }
4622 
4623     /**
4624      * Returns true if shifting drawing offset for start overhang.
4625      *
4626      * @return True if shifting drawing offset for start overhang.
4627      * @see android.widget.TextView#setShiftDrawingOffsetForStartOverhang(boolean)
4628      * @see TextView#getShiftDrawingOffsetForStartOverhang()
4629      * @see StaticLayout.Builder#setShiftDrawingOffsetForStartOverhang(boolean)
4630      * @see DynamicLayout.Builder#setShiftDrawingOffsetForStartOverhang(boolean)
4631      */
4632     @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
getShiftDrawingOffsetForStartOverhang()4633     public boolean getShiftDrawingOffsetForStartOverhang() {
4634         return mShiftDrawingOffsetForStartOverhang;
4635     }
4636 
4637     /**
4638      * Get the minimum font metrics used for line spacing.
4639      *
4640      * @see android.widget.TextView#setMinimumFontMetrics(Paint.FontMetrics)
4641      * @see android.widget.TextView#getMinimumFontMetrics()
4642      * @see Layout.Builder#setMinimumFontMetrics(Paint.FontMetrics)
4643      * @see StaticLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics)
4644      * @see DynamicLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics)
4645      *
4646      * @return a minimum font metrics. {@code null} for using the value obtained by
4647      *         {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)}
4648      */
4649     @Nullable
4650     @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE)
getMinimumFontMetrics()4651     public Paint.FontMetrics getMinimumFontMetrics() {
4652         return mMinimumFontMetrics;
4653     }
4654 
4655     /**
4656      * Callback for {@link #forEachCharacterBounds(int, int, int, int, CharacterBoundsListener)}
4657      */
4658     private interface CharacterBoundsListener {
4659         /**
4660          * Called for each character with its bounds.
4661          *
4662          * @param index the index of the character
4663          * @param lineNum the line number of the character
4664          * @param left the left edge of the character
4665          * @param top the top edge of the character
4666          * @param right the right edge of the character
4667          * @param bottom the bottom edge of the character
4668          */
4669         void onCharacterBounds(int index, int lineNum, float left, float top, float right,
4670                 float bottom);
4671 
4672         /** Called after the last character has been sent to {@link #onCharacterBounds}. */
onEnd()4673         default void onEnd() {}
4674     }
4675 }
4676