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