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