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