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