• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.text;
18 
19 import android.emoji.EmojiFactory;
20 import android.graphics.Canvas;
21 import android.graphics.Paint;
22 import android.graphics.Path;
23 import android.graphics.Rect;
24 import android.text.method.TextKeyListener;
25 import android.text.style.AlignmentSpan;
26 import android.text.style.LeadingMarginSpan;
27 import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
28 import android.text.style.LineBackgroundSpan;
29 import android.text.style.ParagraphStyle;
30 import android.text.style.ReplacementSpan;
31 import android.text.style.TabStopSpan;
32 
33 import com.android.internal.util.ArrayUtils;
34 
35 import java.util.Arrays;
36 
37 /**
38  * A base class that manages text layout in visual elements on
39  * the screen.
40  * <p>For text that will be edited, use a {@link DynamicLayout},
41  * which will be updated as the text changes.
42  * For text that will not change, use a {@link StaticLayout}.
43  */
44 public abstract class Layout {
45     private static final ParagraphStyle[] NO_PARA_SPANS =
46         ArrayUtils.emptyArray(ParagraphStyle.class);
47 
48     /* package */ static final EmojiFactory EMOJI_FACTORY = EmojiFactory.newAvailableInstance();
49     /* package */ static final int MIN_EMOJI, MAX_EMOJI;
50 
51     static {
52         if (EMOJI_FACTORY != null) {
53             MIN_EMOJI = EMOJI_FACTORY.getMinimumAndroidPua();
54             MAX_EMOJI = EMOJI_FACTORY.getMaximumAndroidPua();
55         } else {
56             MIN_EMOJI = -1;
57             MAX_EMOJI = -1;
58         }
59     }
60 
61     /**
62      * Return how wide a layout must be in order to display the
63      * specified text with one line per paragraph.
64      */
getDesiredWidth(CharSequence source, TextPaint paint)65     public static float getDesiredWidth(CharSequence source,
66                                         TextPaint paint) {
67         return getDesiredWidth(source, 0, source.length(), paint);
68     }
69 
70     /**
71      * Return how wide a layout must be in order to display the
72      * specified text slice with one line per paragraph.
73      */
getDesiredWidth(CharSequence source, int start, int end, TextPaint paint)74     public static float getDesiredWidth(CharSequence source,
75                                         int start, int end,
76                                         TextPaint paint) {
77         float need = 0;
78 
79         int next;
80         for (int i = start; i <= end; i = next) {
81             next = TextUtils.indexOf(source, '\n', i, end);
82 
83             if (next < 0)
84                 next = end;
85 
86             // note, omits trailing paragraph char
87             float w = measurePara(paint, source, i, next);
88 
89             if (w > need)
90                 need = w;
91 
92             next++;
93         }
94 
95         return need;
96     }
97 
98     /**
99      * Subclasses of Layout use this constructor to set the display text,
100      * width, and other standard properties.
101      * @param text the text to render
102      * @param paint the default paint for the layout.  Styles can override
103      * various attributes of the paint.
104      * @param width the wrapping width for the text.
105      * @param align whether to left, right, or center the text.  Styles can
106      * override the alignment.
107      * @param spacingMult factor by which to scale the font size to get the
108      * default line spacing
109      * @param spacingAdd amount to add to the default line spacing
110      */
Layout(CharSequence text, TextPaint paint, int width, Alignment align, float spacingMult, float spacingAdd)111     protected Layout(CharSequence text, TextPaint paint,
112                      int width, Alignment align,
113                      float spacingMult, float spacingAdd) {
114         this(text, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
115                 spacingMult, spacingAdd);
116     }
117 
118     /**
119      * Subclasses of Layout use this constructor to set the display text,
120      * width, and other standard properties.
121      * @param text the text to render
122      * @param paint the default paint for the layout.  Styles can override
123      * various attributes of the paint.
124      * @param width the wrapping width for the text.
125      * @param align whether to left, right, or center the text.  Styles can
126      * override the alignment.
127      * @param spacingMult factor by which to scale the font size to get the
128      * default line spacing
129      * @param spacingAdd amount to add to the default line spacing
130      *
131      * @hide
132      */
Layout(CharSequence text, TextPaint paint, int width, Alignment align, TextDirectionHeuristic textDir, float spacingMult, float spacingAdd)133     protected Layout(CharSequence text, TextPaint paint,
134                      int width, Alignment align, TextDirectionHeuristic textDir,
135                      float spacingMult, float spacingAdd) {
136 
137         if (width < 0)
138             throw new IllegalArgumentException("Layout: " + width + " < 0");
139 
140         // Ensure paint doesn't have baselineShift set.
141         // While normally we don't modify the paint the user passed in,
142         // we were already doing this in Styled.drawUniformRun with both
143         // baselineShift and bgColor.  We probably should reevaluate bgColor.
144         if (paint != null) {
145             paint.bgColor = 0;
146             paint.baselineShift = 0;
147         }
148 
149         mText = text;
150         mPaint = paint;
151         mWorkPaint = new TextPaint();
152         mWidth = width;
153         mAlignment = align;
154         mSpacingMult = spacingMult;
155         mSpacingAdd = spacingAdd;
156         mSpannedText = text instanceof Spanned;
157         mTextDir = textDir;
158     }
159 
160     /**
161      * Replace constructor properties of this Layout with new ones.  Be careful.
162      */
replaceWith(CharSequence text, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd)163     /* package */ void replaceWith(CharSequence text, TextPaint paint,
164                               int width, Alignment align,
165                               float spacingmult, float spacingadd) {
166         if (width < 0) {
167             throw new IllegalArgumentException("Layout: " + width + " < 0");
168         }
169 
170         mText = text;
171         mPaint = paint;
172         mWidth = width;
173         mAlignment = align;
174         mSpacingMult = spacingmult;
175         mSpacingAdd = spacingadd;
176         mSpannedText = text instanceof Spanned;
177     }
178 
179     /**
180      * Draw this Layout on the specified Canvas.
181      */
draw(Canvas c)182     public void draw(Canvas c) {
183         draw(c, null, null, 0);
184     }
185 
186     /**
187      * Draw this Layout on the specified canvas, with the highlight path drawn
188      * between the background and the text.
189      *
190      * @param canvas the canvas
191      * @param highlight the path of the highlight or cursor; can be null
192      * @param highlightPaint the paint for the highlight
193      * @param cursorOffsetVertical the amount to temporarily translate the
194      *        canvas while rendering the highlight
195      */
draw(Canvas canvas, Path highlight, Paint highlightPaint, int cursorOffsetVertical)196     public void draw(Canvas canvas, Path highlight, Paint highlightPaint,
197             int cursorOffsetVertical) {
198         final long lineRange = getLineRangeForDraw(canvas);
199         int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
200         int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
201         if (lastLine < 0) return;
202 
203         drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
204                 firstLine, lastLine);
205         drawText(canvas, firstLine, lastLine);
206     }
207 
208     /**
209      * @hide
210      */
drawText(Canvas canvas, int firstLine, int lastLine)211     public void drawText(Canvas canvas, int firstLine, int lastLine) {
212         int previousLineBottom = getLineTop(firstLine);
213         int previousLineEnd = getLineStart(firstLine);
214         ParagraphStyle[] spans = NO_PARA_SPANS;
215         int spanEnd = 0;
216         TextPaint paint = mPaint;
217         CharSequence buf = mText;
218 
219         Alignment paraAlign = mAlignment;
220         TabStops tabStops = null;
221         boolean tabStopsIsInitialized = false;
222 
223         TextLine tl = TextLine.obtain();
224 
225         // Draw the lines, one at a time.
226         // The baseline is the top of the following line minus the current line's descent.
227         for (int i = firstLine; i <= lastLine; i++) {
228             int start = previousLineEnd;
229             previousLineEnd = getLineStart(i + 1);
230             int end = getLineVisibleEnd(i, start, previousLineEnd);
231 
232             int ltop = previousLineBottom;
233             int lbottom = getLineTop(i+1);
234             previousLineBottom = lbottom;
235             int lbaseline = lbottom - getLineDescent(i);
236 
237             int dir = getParagraphDirection(i);
238             int left = 0;
239             int right = mWidth;
240 
241             if (mSpannedText) {
242                 Spanned sp = (Spanned) buf;
243                 int textLength = buf.length();
244                 boolean isFirstParaLine = (start == 0 || buf.charAt(start - 1) == '\n');
245 
246                 // New batch of paragraph styles, collect into spans array.
247                 // Compute the alignment, last alignment style wins.
248                 // Reset tabStops, we'll rebuild if we encounter a line with
249                 // tabs.
250                 // We expect paragraph spans to be relatively infrequent, use
251                 // spanEnd so that we can check less frequently.  Since
252                 // paragraph styles ought to apply to entire paragraphs, we can
253                 // just collect the ones present at the start of the paragraph.
254                 // If spanEnd is before the end of the paragraph, that's not
255                 // our problem.
256                 if (start >= spanEnd && (i == firstLine || isFirstParaLine)) {
257                     spanEnd = sp.nextSpanTransition(start, textLength,
258                                                     ParagraphStyle.class);
259                     spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);
260 
261                     paraAlign = mAlignment;
262                     for (int n = spans.length - 1; n >= 0; n--) {
263                         if (spans[n] instanceof AlignmentSpan) {
264                             paraAlign = ((AlignmentSpan) spans[n]).getAlignment();
265                             break;
266                         }
267                     }
268 
269                     tabStopsIsInitialized = false;
270                 }
271 
272                 // Draw all leading margin spans.  Adjust left or right according
273                 // to the paragraph direction of the line.
274                 final int length = spans.length;
275                 for (int n = 0; n < length; n++) {
276                     if (spans[n] instanceof LeadingMarginSpan) {
277                         LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
278                         boolean useFirstLineMargin = isFirstParaLine;
279                         if (margin instanceof LeadingMarginSpan2) {
280                             int count = ((LeadingMarginSpan2) margin).getLeadingMarginLineCount();
281                             int startLine = getLineForOffset(sp.getSpanStart(margin));
282                             useFirstLineMargin = i < startLine + count;
283                         }
284 
285                         if (dir == DIR_RIGHT_TO_LEFT) {
286                             margin.drawLeadingMargin(canvas, paint, right, dir, ltop,
287                                                      lbaseline, lbottom, buf,
288                                                      start, end, isFirstParaLine, this);
289                             right -= margin.getLeadingMargin(useFirstLineMargin);
290                         } else {
291                             margin.drawLeadingMargin(canvas, paint, left, dir, ltop,
292                                                      lbaseline, lbottom, buf,
293                                                      start, end, isFirstParaLine, this);
294                             left += margin.getLeadingMargin(useFirstLineMargin);
295                         }
296                     }
297                 }
298             }
299 
300             boolean hasTabOrEmoji = getLineContainsTab(i);
301             // Can't tell if we have tabs for sure, currently
302             if (hasTabOrEmoji && !tabStopsIsInitialized) {
303                 if (tabStops == null) {
304                     tabStops = new TabStops(TAB_INCREMENT, spans);
305                 } else {
306                     tabStops.reset(TAB_INCREMENT, spans);
307                 }
308                 tabStopsIsInitialized = true;
309             }
310 
311             // Determine whether the line aligns to normal, opposite, or center.
312             Alignment align = paraAlign;
313             if (align == Alignment.ALIGN_LEFT) {
314                 align = (dir == DIR_LEFT_TO_RIGHT) ?
315                     Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
316             } else if (align == Alignment.ALIGN_RIGHT) {
317                 align = (dir == DIR_LEFT_TO_RIGHT) ?
318                     Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
319             }
320 
321             int x;
322             if (align == Alignment.ALIGN_NORMAL) {
323                 if (dir == DIR_LEFT_TO_RIGHT) {
324                     x = left;
325                 } else {
326                     x = right;
327                 }
328             } else {
329                 int max = (int)getLineExtent(i, tabStops, false);
330                 if (align == Alignment.ALIGN_OPPOSITE) {
331                     if (dir == DIR_LEFT_TO_RIGHT) {
332                         x = right - max;
333                     } else {
334                         x = left - max;
335                     }
336                 } else { // Alignment.ALIGN_CENTER
337                     max = max & ~1;
338                     x = (right + left - max) >> 1;
339                 }
340             }
341 
342             Directions directions = getLineDirections(i);
343             if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTabOrEmoji) {
344                 // XXX: assumes there's nothing additional to be done
345                 canvas.drawText(buf, start, end, x, lbaseline, paint);
346             } else {
347                 tl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops);
348                 tl.draw(canvas, x, ltop, lbaseline, lbottom);
349             }
350         }
351 
352         TextLine.recycle(tl);
353     }
354 
355     /**
356      * @hide
357      */
drawBackground(Canvas canvas, Path highlight, Paint highlightPaint, int cursorOffsetVertical, int firstLine, int lastLine)358     public void drawBackground(Canvas canvas, Path highlight, Paint highlightPaint,
359             int cursorOffsetVertical, int firstLine, int lastLine) {
360         // First, draw LineBackgroundSpans.
361         // LineBackgroundSpans know nothing about the alignment, margins, or
362         // direction of the layout or line.  XXX: Should they?
363         // They are evaluated at each line.
364         if (mSpannedText) {
365             if (mLineBackgroundSpans == null) {
366                 mLineBackgroundSpans = new SpanSet<LineBackgroundSpan>(LineBackgroundSpan.class);
367             }
368 
369             Spanned buffer = (Spanned) mText;
370             int textLength = buffer.length();
371             mLineBackgroundSpans.init(buffer, 0, textLength);
372 
373             if (mLineBackgroundSpans.numberOfSpans > 0) {
374                 int previousLineBottom = getLineTop(firstLine);
375                 int previousLineEnd = getLineStart(firstLine);
376                 ParagraphStyle[] spans = NO_PARA_SPANS;
377                 int spansLength = 0;
378                 TextPaint paint = mPaint;
379                 int spanEnd = 0;
380                 final int width = mWidth;
381                 for (int i = firstLine; i <= lastLine; i++) {
382                     int start = previousLineEnd;
383                     int end = getLineStart(i + 1);
384                     previousLineEnd = end;
385 
386                     int ltop = previousLineBottom;
387                     int lbottom = getLineTop(i + 1);
388                     previousLineBottom = lbottom;
389                     int lbaseline = lbottom - getLineDescent(i);
390 
391                     if (start >= spanEnd) {
392                         // These should be infrequent, so we'll use this so that
393                         // we don't have to check as often.
394                         spanEnd = mLineBackgroundSpans.getNextTransition(start, textLength);
395                         // All LineBackgroundSpans on a line contribute to its background.
396                         spansLength = 0;
397                         // Duplication of the logic of getParagraphSpans
398                         if (start != end || start == 0) {
399                             // Equivalent to a getSpans(start, end), but filling the 'spans' local
400                             // array instead to reduce memory allocation
401                             for (int j = 0; j < mLineBackgroundSpans.numberOfSpans; j++) {
402                                 // equal test is valid since both intervals are not empty by
403                                 // construction
404                                 if (mLineBackgroundSpans.spanStarts[j] >= end ||
405                                         mLineBackgroundSpans.spanEnds[j] <= start) continue;
406                                 if (spansLength == spans.length) {
407                                     // The spans array needs to be expanded
408                                     int newSize = ArrayUtils.idealObjectArraySize(2 * spansLength);
409                                     ParagraphStyle[] newSpans = new ParagraphStyle[newSize];
410                                     System.arraycopy(spans, 0, newSpans, 0, spansLength);
411                                     spans = newSpans;
412                                 }
413                                 spans[spansLength++] = mLineBackgroundSpans.spans[j];
414                             }
415                         }
416                     }
417 
418                     for (int n = 0; n < spansLength; n++) {
419                         LineBackgroundSpan lineBackgroundSpan = (LineBackgroundSpan) spans[n];
420                         lineBackgroundSpan.drawBackground(canvas, paint, 0, width,
421                                 ltop, lbaseline, lbottom,
422                                 buffer, start, end, i);
423                     }
424                 }
425             }
426             mLineBackgroundSpans.recycle();
427         }
428 
429         // There can be a highlight even without spans if we are drawing
430         // a non-spanned transformation of a spanned editing buffer.
431         if (highlight != null) {
432             if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical);
433             canvas.drawPath(highlight, highlightPaint);
434             if (cursorOffsetVertical != 0) canvas.translate(0, -cursorOffsetVertical);
435         }
436     }
437 
438     /**
439      * @param canvas
440      * @return The range of lines that need to be drawn, possibly empty.
441      * @hide
442      */
getLineRangeForDraw(Canvas canvas)443     public long getLineRangeForDraw(Canvas canvas) {
444         int dtop, dbottom;
445 
446         synchronized (sTempRect) {
447             if (!canvas.getClipBounds(sTempRect)) {
448                 // Negative range end used as a special flag
449                 return TextUtils.packRangeInLong(0, -1);
450             }
451 
452             dtop = sTempRect.top;
453             dbottom = sTempRect.bottom;
454         }
455 
456         final int top = Math.max(dtop, 0);
457         final int bottom = Math.min(getLineTop(getLineCount()), dbottom);
458 
459         if (top >= bottom) return TextUtils.packRangeInLong(0, -1);
460         return TextUtils.packRangeInLong(getLineForVertical(top), getLineForVertical(bottom));
461     }
462 
463     /**
464      * Return the start position of the line, given the left and right bounds
465      * of the margins.
466      *
467      * @param line the line index
468      * @param left the left bounds (0, or leading margin if ltr para)
469      * @param right the right bounds (width, minus leading margin if rtl para)
470      * @return the start position of the line (to right of line if rtl para)
471      */
getLineStartPos(int line, int left, int right)472     private int getLineStartPos(int line, int left, int right) {
473         // Adjust the point at which to start rendering depending on the
474         // alignment of the paragraph.
475         Alignment align = getParagraphAlignment(line);
476         int dir = getParagraphDirection(line);
477 
478         if (align == Alignment.ALIGN_LEFT) {
479             align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
480         } else if (align == Alignment.ALIGN_RIGHT) {
481             align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
482         }
483 
484         int x;
485         if (align == Alignment.ALIGN_NORMAL) {
486             if (dir == DIR_LEFT_TO_RIGHT) {
487                 x = left;
488             } else {
489                 x = right;
490             }
491         } else {
492             TabStops tabStops = null;
493             if (mSpannedText && getLineContainsTab(line)) {
494                 Spanned spanned = (Spanned) mText;
495                 int start = getLineStart(line);
496                 int spanEnd = spanned.nextSpanTransition(start, spanned.length(),
497                         TabStopSpan.class);
498                 TabStopSpan[] tabSpans = getParagraphSpans(spanned, start, spanEnd,
499                         TabStopSpan.class);
500                 if (tabSpans.length > 0) {
501                     tabStops = new TabStops(TAB_INCREMENT, tabSpans);
502                 }
503             }
504             int max = (int)getLineExtent(line, tabStops, false);
505             if (align == Alignment.ALIGN_OPPOSITE) {
506                 if (dir == DIR_LEFT_TO_RIGHT) {
507                     x = right - max;
508                 } else {
509                     // max is negative here
510                     x = left - max;
511                 }
512             } else { // Alignment.ALIGN_CENTER
513                 max = max & ~1;
514                 x = (left + right - max) >> 1;
515             }
516         }
517         return x;
518     }
519 
520     /**
521      * Return the text that is displayed by this Layout.
522      */
getText()523     public final CharSequence getText() {
524         return mText;
525     }
526 
527     /**
528      * Return the base Paint properties for this layout.
529      * Do NOT change the paint, which may result in funny
530      * drawing for this layout.
531      */
getPaint()532     public final TextPaint getPaint() {
533         return mPaint;
534     }
535 
536     /**
537      * Return the width of this layout.
538      */
getWidth()539     public final int getWidth() {
540         return mWidth;
541     }
542 
543     /**
544      * Return the width to which this Layout is ellipsizing, or
545      * {@link #getWidth} if it is not doing anything special.
546      */
getEllipsizedWidth()547     public int getEllipsizedWidth() {
548         return mWidth;
549     }
550 
551     /**
552      * Increase the width of this layout to the specified width.
553      * Be careful to use this only when you know it is appropriate&mdash;
554      * it does not cause the text to reflow to use the full new width.
555      */
increaseWidthTo(int wid)556     public final void increaseWidthTo(int wid) {
557         if (wid < mWidth) {
558             throw new RuntimeException("attempted to reduce Layout width");
559         }
560 
561         mWidth = wid;
562     }
563 
564     /**
565      * Return the total height of this layout.
566      */
getHeight()567     public int getHeight() {
568         return getLineTop(getLineCount());
569     }
570 
571     /**
572      * Return the base alignment of this layout.
573      */
getAlignment()574     public final Alignment getAlignment() {
575         return mAlignment;
576     }
577 
578     /**
579      * Return what the text height is multiplied by to get the line height.
580      */
getSpacingMultiplier()581     public final float getSpacingMultiplier() {
582         return mSpacingMult;
583     }
584 
585     /**
586      * Return the number of units of leading that are added to each line.
587      */
getSpacingAdd()588     public final float getSpacingAdd() {
589         return mSpacingAdd;
590     }
591 
592     /**
593      * Return the heuristic used to determine paragraph text direction.
594      * @hide
595      */
getTextDirectionHeuristic()596     public final TextDirectionHeuristic getTextDirectionHeuristic() {
597         return mTextDir;
598     }
599 
600     /**
601      * Return the number of lines of text in this layout.
602      */
603     public abstract int getLineCount();
604 
605     /**
606      * Return the baseline for the specified line (0&hellip;getLineCount() - 1)
607      * If bounds is not null, return the top, left, right, bottom extents
608      * of the specified line in it.
609      * @param line which line to examine (0..getLineCount() - 1)
610      * @param bounds Optional. If not null, it returns the extent of the line
611      * @return the Y-coordinate of the baseline
612      */
getLineBounds(int line, Rect bounds)613     public int getLineBounds(int line, Rect bounds) {
614         if (bounds != null) {
615             bounds.left = 0;     // ???
616             bounds.top = getLineTop(line);
617             bounds.right = mWidth;   // ???
618             bounds.bottom = getLineTop(line + 1);
619         }
620         return getLineBaseline(line);
621     }
622 
623     /**
624      * Return the vertical position of the top of the specified line
625      * (0&hellip;getLineCount()).
626      * If the specified line is equal to the line count, returns the
627      * bottom of the last line.
628      */
629     public abstract int getLineTop(int line);
630 
631     /**
632      * Return the descent of the specified line(0&hellip;getLineCount() - 1).
633      */
634     public abstract int getLineDescent(int line);
635 
636     /**
637      * Return the text offset of the beginning of the specified line (
638      * 0&hellip;getLineCount()). If the specified line is equal to the line
639      * count, returns the length of the text.
640      */
641     public abstract int getLineStart(int line);
642 
643     /**
644      * Returns the primary directionality of the paragraph containing the
645      * specified line, either 1 for left-to-right lines, or -1 for right-to-left
646      * lines (see {@link #DIR_LEFT_TO_RIGHT}, {@link #DIR_RIGHT_TO_LEFT}).
647      */
648     public abstract int getParagraphDirection(int line);
649 
650     /**
651      * Returns whether the specified line contains one or more
652      * characters that need to be handled specially, like tabs
653      * or emoji.
654      */
655     public abstract boolean getLineContainsTab(int line);
656 
657     /**
658      * Returns the directional run information for the specified line.
659      * The array alternates counts of characters in left-to-right
660      * and right-to-left segments of the line.
661      *
662      * <p>NOTE: this is inadequate to support bidirectional text, and will change.
663      */
664     public abstract Directions getLineDirections(int line);
665 
666     /**
667      * Returns the (negative) number of extra pixels of ascent padding in the
668      * top line of the Layout.
669      */
670     public abstract int getTopPadding();
671 
672     /**
673      * Returns the number of extra pixels of descent padding in the
674      * bottom line of the Layout.
675      */
676     public abstract int getBottomPadding();
677 
678 
679     /**
680      * Returns true if the character at offset and the preceding character
681      * are at different run levels (and thus there's a split caret).
682      * @param offset the offset
683      * @return true if at a level boundary
684      * @hide
685      */
isLevelBoundary(int offset)686     public boolean isLevelBoundary(int offset) {
687         int line = getLineForOffset(offset);
688         Directions dirs = getLineDirections(line);
689         if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) {
690             return false;
691         }
692 
693         int[] runs = dirs.mDirections;
694         int lineStart = getLineStart(line);
695         int lineEnd = getLineEnd(line);
696         if (offset == lineStart || offset == lineEnd) {
697             int paraLevel = getParagraphDirection(line) == 1 ? 0 : 1;
698             int runIndex = offset == lineStart ? 0 : runs.length - 2;
699             return ((runs[runIndex + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK) != paraLevel;
700         }
701 
702         offset -= lineStart;
703         for (int i = 0; i < runs.length; i += 2) {
704             if (offset == runs[i]) {
705                 return true;
706             }
707         }
708         return false;
709     }
710 
711     /**
712      * Returns true if the character at offset is right to left (RTL).
713      * @param offset the offset
714      * @return true if the character is RTL, false if it is LTR
715      */
isRtlCharAt(int offset)716     public boolean isRtlCharAt(int offset) {
717         int line = getLineForOffset(offset);
718         Directions dirs = getLineDirections(line);
719         if (dirs == DIRS_ALL_LEFT_TO_RIGHT) {
720             return false;
721         }
722         if (dirs == DIRS_ALL_RIGHT_TO_LEFT) {
723             return  true;
724         }
725         int[] runs = dirs.mDirections;
726         int lineStart = getLineStart(line);
727         for (int i = 0; i < runs.length; i += 2) {
728             int start = lineStart + (runs[i] & RUN_LENGTH_MASK);
729             // No need to test the end as an offset after the last run should return the value
730             // corresponding of the last run
731             if (offset >= start) {
732                 int level = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
733                 return ((level & 1) != 0);
734             }
735         }
736         // Should happen only if the offset is "out of bounds"
737         return false;
738     }
739 
primaryIsTrailingPrevious(int offset)740     private boolean primaryIsTrailingPrevious(int offset) {
741         int line = getLineForOffset(offset);
742         int lineStart = getLineStart(line);
743         int lineEnd = getLineEnd(line);
744         int[] runs = getLineDirections(line).mDirections;
745 
746         int levelAt = -1;
747         for (int i = 0; i < runs.length; i += 2) {
748             int start = lineStart + runs[i];
749             int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
750             if (limit > lineEnd) {
751                 limit = lineEnd;
752             }
753             if (offset >= start && offset < limit) {
754                 if (offset > start) {
755                     // Previous character is at same level, so don't use trailing.
756                     return false;
757                 }
758                 levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
759                 break;
760             }
761         }
762         if (levelAt == -1) {
763             // Offset was limit of line.
764             levelAt = getParagraphDirection(line) == 1 ? 0 : 1;
765         }
766 
767         // At level boundary, check previous level.
768         int levelBefore = -1;
769         if (offset == lineStart) {
770             levelBefore = getParagraphDirection(line) == 1 ? 0 : 1;
771         } else {
772             offset -= 1;
773             for (int i = 0; i < runs.length; i += 2) {
774                 int start = lineStart + runs[i];
775                 int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
776                 if (limit > lineEnd) {
777                     limit = lineEnd;
778                 }
779                 if (offset >= start && offset < limit) {
780                     levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
781                     break;
782                 }
783             }
784         }
785 
786         return levelBefore < levelAt;
787     }
788 
789     /**
790      * Get the primary horizontal position for the specified text offset.
791      * This is the location where a new character would be inserted in
792      * the paragraph's primary direction.
793      */
getPrimaryHorizontal(int offset)794     public float getPrimaryHorizontal(int offset) {
795         boolean trailing = primaryIsTrailingPrevious(offset);
796         return getHorizontal(offset, trailing);
797     }
798 
799     /**
800      * Get the secondary horizontal position for the specified text offset.
801      * This is the location where a new character would be inserted in
802      * the direction other than the paragraph's primary direction.
803      */
getSecondaryHorizontal(int offset)804     public float getSecondaryHorizontal(int offset) {
805         boolean trailing = primaryIsTrailingPrevious(offset);
806         return getHorizontal(offset, !trailing);
807     }
808 
getHorizontal(int offset, boolean trailing)809     private float getHorizontal(int offset, boolean trailing) {
810         int line = getLineForOffset(offset);
811 
812         return getHorizontal(offset, trailing, line);
813     }
814 
getHorizontal(int offset, boolean trailing, int line)815     private float getHorizontal(int offset, boolean trailing, int line) {
816         int start = getLineStart(line);
817         int end = getLineEnd(line);
818         int dir = getParagraphDirection(line);
819         boolean hasTabOrEmoji = getLineContainsTab(line);
820         Directions directions = getLineDirections(line);
821 
822         TabStops tabStops = null;
823         if (hasTabOrEmoji && mText instanceof Spanned) {
824             // Just checking this line should be good enough, tabs should be
825             // consistent across all lines in a paragraph.
826             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
827             if (tabs.length > 0) {
828                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
829             }
830         }
831 
832         TextLine tl = TextLine.obtain();
833         tl.set(mPaint, mText, start, end, dir, directions, hasTabOrEmoji, tabStops);
834         float wid = tl.measure(offset - start, trailing, null);
835         TextLine.recycle(tl);
836 
837         int left = getParagraphLeft(line);
838         int right = getParagraphRight(line);
839 
840         return getLineStartPos(line, left, right) + wid;
841     }
842 
843     /**
844      * Get the leftmost position that should be exposed for horizontal
845      * scrolling on the specified line.
846      */
getLineLeft(int line)847     public float getLineLeft(int line) {
848         int dir = getParagraphDirection(line);
849         Alignment align = getParagraphAlignment(line);
850 
851         if (align == Alignment.ALIGN_LEFT) {
852             return 0;
853         } else if (align == Alignment.ALIGN_NORMAL) {
854             if (dir == DIR_RIGHT_TO_LEFT)
855                 return getParagraphRight(line) - getLineMax(line);
856             else
857                 return 0;
858         } else if (align == Alignment.ALIGN_RIGHT) {
859             return mWidth - getLineMax(line);
860         } else if (align == Alignment.ALIGN_OPPOSITE) {
861             if (dir == DIR_RIGHT_TO_LEFT)
862                 return 0;
863             else
864                 return mWidth - getLineMax(line);
865         } else { /* align == Alignment.ALIGN_CENTER */
866             int left = getParagraphLeft(line);
867             int right = getParagraphRight(line);
868             int max = ((int) getLineMax(line)) & ~1;
869 
870             return left + ((right - left) - max) / 2;
871         }
872     }
873 
874     /**
875      * Get the rightmost position that should be exposed for horizontal
876      * scrolling on the specified line.
877      */
getLineRight(int line)878     public float getLineRight(int line) {
879         int dir = getParagraphDirection(line);
880         Alignment align = getParagraphAlignment(line);
881 
882         if (align == Alignment.ALIGN_LEFT) {
883             return getParagraphLeft(line) + getLineMax(line);
884         } else if (align == Alignment.ALIGN_NORMAL) {
885             if (dir == DIR_RIGHT_TO_LEFT)
886                 return mWidth;
887             else
888                 return getParagraphLeft(line) + getLineMax(line);
889         } else if (align == Alignment.ALIGN_RIGHT) {
890             return mWidth;
891         } else if (align == Alignment.ALIGN_OPPOSITE) {
892             if (dir == DIR_RIGHT_TO_LEFT)
893                 return getLineMax(line);
894             else
895                 return mWidth;
896         } else { /* align == Alignment.ALIGN_CENTER */
897             int left = getParagraphLeft(line);
898             int right = getParagraphRight(line);
899             int max = ((int) getLineMax(line)) & ~1;
900 
901             return right - ((right - left) - max) / 2;
902         }
903     }
904 
905     /**
906      * Gets the unsigned horizontal extent of the specified line, including
907      * leading margin indent, but excluding trailing whitespace.
908      */
getLineMax(int line)909     public float getLineMax(int line) {
910         float margin = getParagraphLeadingMargin(line);
911         float signedExtent = getLineExtent(line, false);
912         return margin + signedExtent >= 0 ? signedExtent : -signedExtent;
913     }
914 
915     /**
916      * Gets the unsigned horizontal extent of the specified line, including
917      * leading margin indent and trailing whitespace.
918      */
getLineWidth(int line)919     public float getLineWidth(int line) {
920         float margin = getParagraphLeadingMargin(line);
921         float signedExtent = getLineExtent(line, true);
922         return margin + signedExtent >= 0 ? signedExtent : -signedExtent;
923     }
924 
925     /**
926      * Like {@link #getLineExtent(int,TabStops,boolean)} but determines the
927      * tab stops instead of using the ones passed in.
928      * @param line the index of the line
929      * @param full whether to include trailing whitespace
930      * @return the extent of the line
931      */
getLineExtent(int line, boolean full)932     private float getLineExtent(int line, boolean full) {
933         int start = getLineStart(line);
934         int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
935 
936         boolean hasTabsOrEmoji = getLineContainsTab(line);
937         TabStops tabStops = null;
938         if (hasTabsOrEmoji && mText instanceof Spanned) {
939             // Just checking this line should be good enough, tabs should be
940             // consistent across all lines in a paragraph.
941             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
942             if (tabs.length > 0) {
943                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
944             }
945         }
946         Directions directions = getLineDirections(line);
947         // Returned directions can actually be null
948         if (directions == null) {
949             return 0f;
950         }
951         int dir = getParagraphDirection(line);
952 
953         TextLine tl = TextLine.obtain();
954         tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops);
955         float width = tl.metrics(null);
956         TextLine.recycle(tl);
957         return width;
958     }
959 
960     /**
961      * Returns the signed horizontal extent of the specified line, excluding
962      * leading margin.  If full is false, excludes trailing whitespace.
963      * @param line the index of the line
964      * @param tabStops the tab stops, can be null if we know they're not used.
965      * @param full whether to include trailing whitespace
966      * @return the extent of the text on this line
967      */
getLineExtent(int line, TabStops tabStops, boolean full)968     private float getLineExtent(int line, TabStops tabStops, boolean full) {
969         int start = getLineStart(line);
970         int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
971         boolean hasTabsOrEmoji = getLineContainsTab(line);
972         Directions directions = getLineDirections(line);
973         int dir = getParagraphDirection(line);
974 
975         TextLine tl = TextLine.obtain();
976         tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops);
977         float width = tl.metrics(null);
978         TextLine.recycle(tl);
979         return width;
980     }
981 
982     /**
983      * Get the line number corresponding to the specified vertical position.
984      * If you ask for a position above 0, you get 0; if you ask for a position
985      * below the bottom of the text, you get the last line.
986      */
987     // FIXME: It may be faster to do a linear search for layouts without many lines.
getLineForVertical(int vertical)988     public int getLineForVertical(int vertical) {
989         int high = getLineCount(), low = -1, guess;
990 
991         while (high - low > 1) {
992             guess = (high + low) / 2;
993 
994             if (getLineTop(guess) > vertical)
995                 high = guess;
996             else
997                 low = guess;
998         }
999 
1000         if (low < 0)
1001             return 0;
1002         else
1003             return low;
1004     }
1005 
1006     /**
1007      * Get the line number on which the specified text offset appears.
1008      * If you ask for a position before 0, you get 0; if you ask for a position
1009      * beyond the end of the text, you get the last line.
1010      */
getLineForOffset(int offset)1011     public int getLineForOffset(int offset) {
1012         int high = getLineCount(), low = -1, guess;
1013 
1014         while (high - low > 1) {
1015             guess = (high + low) / 2;
1016 
1017             if (getLineStart(guess) > offset)
1018                 high = guess;
1019             else
1020                 low = guess;
1021         }
1022 
1023         if (low < 0)
1024             return 0;
1025         else
1026             return low;
1027     }
1028 
1029     /**
1030      * Get the character offset on the specified line whose position is
1031      * closest to the specified horizontal position.
1032      */
getOffsetForHorizontal(int line, float horiz)1033     public int getOffsetForHorizontal(int line, float horiz) {
1034         int max = getLineEnd(line) - 1;
1035         int min = getLineStart(line);
1036         Directions dirs = getLineDirections(line);
1037 
1038         if (line == getLineCount() - 1)
1039             max++;
1040 
1041         int best = min;
1042         float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz);
1043 
1044         for (int i = 0; i < dirs.mDirections.length; i += 2) {
1045             int here = min + dirs.mDirections[i];
1046             int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK);
1047             int swap = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0 ? -1 : 1;
1048 
1049             if (there > max)
1050                 there = max;
1051             int high = there - 1 + 1, low = here + 1 - 1, guess;
1052 
1053             while (high - low > 1) {
1054                 guess = (high + low) / 2;
1055                 int adguess = getOffsetAtStartOf(guess);
1056 
1057                 if (getPrimaryHorizontal(adguess) * swap >= horiz * swap)
1058                     high = guess;
1059                 else
1060                     low = guess;
1061             }
1062 
1063             if (low < here + 1)
1064                 low = here + 1;
1065 
1066             if (low < there) {
1067                 low = getOffsetAtStartOf(low);
1068 
1069                 float dist = Math.abs(getPrimaryHorizontal(low) - horiz);
1070 
1071                 int aft = TextUtils.getOffsetAfter(mText, low);
1072                 if (aft < there) {
1073                     float other = Math.abs(getPrimaryHorizontal(aft) - horiz);
1074 
1075                     if (other < dist) {
1076                         dist = other;
1077                         low = aft;
1078                     }
1079                 }
1080 
1081                 if (dist < bestdist) {
1082                     bestdist = dist;
1083                     best = low;
1084                 }
1085             }
1086 
1087             float dist = Math.abs(getPrimaryHorizontal(here) - horiz);
1088 
1089             if (dist < bestdist) {
1090                 bestdist = dist;
1091                 best = here;
1092             }
1093         }
1094 
1095         float dist = Math.abs(getPrimaryHorizontal(max) - horiz);
1096 
1097         if (dist < bestdist) {
1098             bestdist = dist;
1099             best = max;
1100         }
1101 
1102         return best;
1103     }
1104 
1105     /**
1106      * Return the text offset after the last character on the specified line.
1107      */
getLineEnd(int line)1108     public final int getLineEnd(int line) {
1109         return getLineStart(line + 1);
1110     }
1111 
1112     /**
1113      * Return the text offset after the last visible character (so whitespace
1114      * is not counted) on the specified line.
1115      */
getLineVisibleEnd(int line)1116     public int getLineVisibleEnd(int line) {
1117         return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1));
1118     }
1119 
getLineVisibleEnd(int line, int start, int end)1120     private int getLineVisibleEnd(int line, int start, int end) {
1121         CharSequence text = mText;
1122         char ch;
1123         if (line == getLineCount() - 1) {
1124             return end;
1125         }
1126 
1127         for (; end > start; end--) {
1128             ch = text.charAt(end - 1);
1129 
1130             if (ch == '\n') {
1131                 return end - 1;
1132             }
1133 
1134             if (ch != ' ' && ch != '\t') {
1135                 break;
1136             }
1137 
1138         }
1139 
1140         return end;
1141     }
1142 
1143     /**
1144      * Return the vertical position of the bottom of the specified line.
1145      */
getLineBottom(int line)1146     public final int getLineBottom(int line) {
1147         return getLineTop(line + 1);
1148     }
1149 
1150     /**
1151      * Return the vertical position of the baseline of the specified line.
1152      */
getLineBaseline(int line)1153     public final int getLineBaseline(int line) {
1154         // getLineTop(line+1) == getLineTop(line)
1155         return getLineTop(line+1) - getLineDescent(line);
1156     }
1157 
1158     /**
1159      * Get the ascent of the text on the specified line.
1160      * The return value is negative to match the Paint.ascent() convention.
1161      */
getLineAscent(int line)1162     public final int getLineAscent(int line) {
1163         // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line)
1164         return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line));
1165     }
1166 
getOffsetToLeftOf(int offset)1167     public int getOffsetToLeftOf(int offset) {
1168         return getOffsetToLeftRightOf(offset, true);
1169     }
1170 
getOffsetToRightOf(int offset)1171     public int getOffsetToRightOf(int offset) {
1172         return getOffsetToLeftRightOf(offset, false);
1173     }
1174 
getOffsetToLeftRightOf(int caret, boolean toLeft)1175     private int getOffsetToLeftRightOf(int caret, boolean toLeft) {
1176         int line = getLineForOffset(caret);
1177         int lineStart = getLineStart(line);
1178         int lineEnd = getLineEnd(line);
1179         int lineDir = getParagraphDirection(line);
1180 
1181         boolean lineChanged = false;
1182         boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT);
1183         // if walking off line, look at the line we're headed to
1184         if (advance) {
1185             if (caret == lineEnd) {
1186                 if (line < getLineCount() - 1) {
1187                     lineChanged = true;
1188                     ++line;
1189                 } else {
1190                     return caret; // at very end, don't move
1191                 }
1192             }
1193         } else {
1194             if (caret == lineStart) {
1195                 if (line > 0) {
1196                     lineChanged = true;
1197                     --line;
1198                 } else {
1199                     return caret; // at very start, don't move
1200                 }
1201             }
1202         }
1203 
1204         if (lineChanged) {
1205             lineStart = getLineStart(line);
1206             lineEnd = getLineEnd(line);
1207             int newDir = getParagraphDirection(line);
1208             if (newDir != lineDir) {
1209                 // unusual case.  we want to walk onto the line, but it runs
1210                 // in a different direction than this one, so we fake movement
1211                 // in the opposite direction.
1212                 toLeft = !toLeft;
1213                 lineDir = newDir;
1214             }
1215         }
1216 
1217         Directions directions = getLineDirections(line);
1218 
1219         TextLine tl = TextLine.obtain();
1220         // XXX: we don't care about tabs
1221         tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null);
1222         caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft);
1223         tl = TextLine.recycle(tl);
1224         return caret;
1225     }
1226 
getOffsetAtStartOf(int offset)1227     private int getOffsetAtStartOf(int offset) {
1228         // XXX this probably should skip local reorderings and
1229         // zero-width characters, look at callers
1230         if (offset == 0)
1231             return 0;
1232 
1233         CharSequence text = mText;
1234         char c = text.charAt(offset);
1235 
1236         if (c >= '\uDC00' && c <= '\uDFFF') {
1237             char c1 = text.charAt(offset - 1);
1238 
1239             if (c1 >= '\uD800' && c1 <= '\uDBFF')
1240                 offset -= 1;
1241         }
1242 
1243         if (mSpannedText) {
1244             ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
1245                                                        ReplacementSpan.class);
1246 
1247             for (int i = 0; i < spans.length; i++) {
1248                 int start = ((Spanned) text).getSpanStart(spans[i]);
1249                 int end = ((Spanned) text).getSpanEnd(spans[i]);
1250 
1251                 if (start < offset && end > offset)
1252                     offset = start;
1253             }
1254         }
1255 
1256         return offset;
1257     }
1258 
1259     /**
1260      * Fills in the specified Path with a representation of a cursor
1261      * at the specified offset.  This will often be a vertical line
1262      * but can be multiple discontinuous lines in text with multiple
1263      * directionalities.
1264      */
getCursorPath(int point, Path dest, CharSequence editingBuffer)1265     public void getCursorPath(int point, Path dest,
1266                               CharSequence editingBuffer) {
1267         dest.reset();
1268 
1269         int line = getLineForOffset(point);
1270         int top = getLineTop(line);
1271         int bottom = getLineTop(line+1);
1272 
1273         float h1 = getPrimaryHorizontal(point) - 0.5f;
1274         float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point) - 0.5f : h1;
1275 
1276         int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) |
1277                    TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING);
1278         int fn = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_ALT_ON);
1279         int dist = 0;
1280 
1281         if (caps != 0 || fn != 0) {
1282             dist = (bottom - top) >> 2;
1283 
1284             if (fn != 0)
1285                 top += dist;
1286             if (caps != 0)
1287                 bottom -= dist;
1288         }
1289 
1290         if (h1 < 0.5f)
1291             h1 = 0.5f;
1292         if (h2 < 0.5f)
1293             h2 = 0.5f;
1294 
1295         if (Float.compare(h1, h2) == 0) {
1296             dest.moveTo(h1, top);
1297             dest.lineTo(h1, bottom);
1298         } else {
1299             dest.moveTo(h1, top);
1300             dest.lineTo(h1, (top + bottom) >> 1);
1301 
1302             dest.moveTo(h2, (top + bottom) >> 1);
1303             dest.lineTo(h2, bottom);
1304         }
1305 
1306         if (caps == 2) {
1307             dest.moveTo(h2, bottom);
1308             dest.lineTo(h2 - dist, bottom + dist);
1309             dest.lineTo(h2, bottom);
1310             dest.lineTo(h2 + dist, bottom + dist);
1311         } else if (caps == 1) {
1312             dest.moveTo(h2, bottom);
1313             dest.lineTo(h2 - dist, bottom + dist);
1314 
1315             dest.moveTo(h2 - dist, bottom + dist - 0.5f);
1316             dest.lineTo(h2 + dist, bottom + dist - 0.5f);
1317 
1318             dest.moveTo(h2 + dist, bottom + dist);
1319             dest.lineTo(h2, bottom);
1320         }
1321 
1322         if (fn == 2) {
1323             dest.moveTo(h1, top);
1324             dest.lineTo(h1 - dist, top - dist);
1325             dest.lineTo(h1, top);
1326             dest.lineTo(h1 + dist, top - dist);
1327         } else if (fn == 1) {
1328             dest.moveTo(h1, top);
1329             dest.lineTo(h1 - dist, top - dist);
1330 
1331             dest.moveTo(h1 - dist, top - dist + 0.5f);
1332             dest.lineTo(h1 + dist, top - dist + 0.5f);
1333 
1334             dest.moveTo(h1 + dist, top - dist);
1335             dest.lineTo(h1, top);
1336         }
1337     }
1338 
addSelection(int line, int start, int end, int top, int bottom, Path dest)1339     private void addSelection(int line, int start, int end,
1340                               int top, int bottom, Path dest) {
1341         int linestart = getLineStart(line);
1342         int lineend = getLineEnd(line);
1343         Directions dirs = getLineDirections(line);
1344 
1345         if (lineend > linestart && mText.charAt(lineend - 1) == '\n')
1346             lineend--;
1347 
1348         for (int i = 0; i < dirs.mDirections.length; i += 2) {
1349             int here = linestart + dirs.mDirections[i];
1350             int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK);
1351 
1352             if (there > lineend)
1353                 there = lineend;
1354 
1355             if (start <= there && end >= here) {
1356                 int st = Math.max(start, here);
1357                 int en = Math.min(end, there);
1358 
1359                 if (st != en) {
1360                     float h1 = getHorizontal(st, false, line);
1361                     float h2 = getHorizontal(en, true, line);
1362 
1363                     float left = Math.min(h1, h2);
1364                     float right = Math.max(h1, h2);
1365 
1366                     dest.addRect(left, top, right, bottom, Path.Direction.CW);
1367                 }
1368             }
1369         }
1370     }
1371 
1372     /**
1373      * Fills in the specified Path with a representation of a highlight
1374      * between the specified offsets.  This will often be a rectangle
1375      * or a potentially discontinuous set of rectangles.  If the start
1376      * and end are the same, the returned path is empty.
1377      */
getSelectionPath(int start, int end, Path dest)1378     public void getSelectionPath(int start, int end, Path dest) {
1379         dest.reset();
1380 
1381         if (start == end)
1382             return;
1383 
1384         if (end < start) {
1385             int temp = end;
1386             end = start;
1387             start = temp;
1388         }
1389 
1390         int startline = getLineForOffset(start);
1391         int endline = getLineForOffset(end);
1392 
1393         int top = getLineTop(startline);
1394         int bottom = getLineBottom(endline);
1395 
1396         if (startline == endline) {
1397             addSelection(startline, start, end, top, bottom, dest);
1398         } else {
1399             final float width = mWidth;
1400 
1401             addSelection(startline, start, getLineEnd(startline),
1402                          top, getLineBottom(startline), dest);
1403 
1404             if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT)
1405                 dest.addRect(getLineLeft(startline), top,
1406                               0, getLineBottom(startline), Path.Direction.CW);
1407             else
1408                 dest.addRect(getLineRight(startline), top,
1409                               width, getLineBottom(startline), Path.Direction.CW);
1410 
1411             for (int i = startline + 1; i < endline; i++) {
1412                 top = getLineTop(i);
1413                 bottom = getLineBottom(i);
1414                 dest.addRect(0, top, width, bottom, Path.Direction.CW);
1415             }
1416 
1417             top = getLineTop(endline);
1418             bottom = getLineBottom(endline);
1419 
1420             addSelection(endline, getLineStart(endline), end,
1421                          top, bottom, dest);
1422 
1423             if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT)
1424                 dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW);
1425             else
1426                 dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW);
1427         }
1428     }
1429 
1430     /**
1431      * Get the alignment of the specified paragraph, taking into account
1432      * markup attached to it.
1433      */
getParagraphAlignment(int line)1434     public final Alignment getParagraphAlignment(int line) {
1435         Alignment align = mAlignment;
1436 
1437         if (mSpannedText) {
1438             Spanned sp = (Spanned) mText;
1439             AlignmentSpan[] spans = getParagraphSpans(sp, getLineStart(line),
1440                                                 getLineEnd(line),
1441                                                 AlignmentSpan.class);
1442 
1443             int spanLength = spans.length;
1444             if (spanLength > 0) {
1445                 align = spans[spanLength-1].getAlignment();
1446             }
1447         }
1448 
1449         return align;
1450     }
1451 
1452     /**
1453      * Get the left edge of the specified paragraph, inset by left margins.
1454      */
getParagraphLeft(int line)1455     public final int getParagraphLeft(int line) {
1456         int left = 0;
1457         int dir = getParagraphDirection(line);
1458         if (dir == DIR_RIGHT_TO_LEFT || !mSpannedText) {
1459             return left; // leading margin has no impact, or no styles
1460         }
1461         return getParagraphLeadingMargin(line);
1462     }
1463 
1464     /**
1465      * Get the right edge of the specified paragraph, inset by right margins.
1466      */
getParagraphRight(int line)1467     public final int getParagraphRight(int line) {
1468         int right = mWidth;
1469         int dir = getParagraphDirection(line);
1470         if (dir == DIR_LEFT_TO_RIGHT || !mSpannedText) {
1471             return right; // leading margin has no impact, or no styles
1472         }
1473         return right - getParagraphLeadingMargin(line);
1474     }
1475 
1476     /**
1477      * Returns the effective leading margin (unsigned) for this line,
1478      * taking into account LeadingMarginSpan and LeadingMarginSpan2.
1479      * @param line the line index
1480      * @return the leading margin of this line
1481      */
getParagraphLeadingMargin(int line)1482     private int getParagraphLeadingMargin(int line) {
1483         if (!mSpannedText) {
1484             return 0;
1485         }
1486         Spanned spanned = (Spanned) mText;
1487 
1488         int lineStart = getLineStart(line);
1489         int lineEnd = getLineEnd(line);
1490         int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd,
1491                 LeadingMarginSpan.class);
1492         LeadingMarginSpan[] spans = getParagraphSpans(spanned, lineStart, spanEnd,
1493                                                 LeadingMarginSpan.class);
1494         if (spans.length == 0) {
1495             return 0; // no leading margin span;
1496         }
1497 
1498         int margin = 0;
1499 
1500         boolean isFirstParaLine = lineStart == 0 ||
1501             spanned.charAt(lineStart - 1) == '\n';
1502 
1503         for (int i = 0; i < spans.length; i++) {
1504             LeadingMarginSpan span = spans[i];
1505             boolean useFirstLineMargin = isFirstParaLine;
1506             if (span instanceof LeadingMarginSpan2) {
1507                 int spStart = spanned.getSpanStart(span);
1508                 int spanLine = getLineForOffset(spStart);
1509                 int count = ((LeadingMarginSpan2)span).getLeadingMarginLineCount();
1510                 useFirstLineMargin = line < spanLine + count;
1511             }
1512             margin += span.getLeadingMargin(useFirstLineMargin);
1513         }
1514 
1515         return margin;
1516     }
1517 
1518     /* package */
1519     static float measurePara(TextPaint paint, CharSequence text, int start, int end) {
1520 
1521         MeasuredText mt = MeasuredText.obtain();
1522         TextLine tl = TextLine.obtain();
1523         try {
1524             mt.setPara(text, start, end, TextDirectionHeuristics.LTR);
1525             Directions directions;
1526             int dir;
1527             if (mt.mEasy) {
1528                 directions = DIRS_ALL_LEFT_TO_RIGHT;
1529                 dir = Layout.DIR_LEFT_TO_RIGHT;
1530             } else {
1531                 directions = AndroidBidi.directions(mt.mDir, mt.mLevels,
1532                     0, mt.mChars, 0, mt.mLen);
1533                 dir = mt.mDir;
1534             }
1535             char[] chars = mt.mChars;
1536             int len = mt.mLen;
1537             boolean hasTabs = false;
1538             TabStops tabStops = null;
1539             for (int i = 0; i < len; ++i) {
1540                 if (chars[i] == '\t') {
1541                     hasTabs = true;
1542                     if (text instanceof Spanned) {
1543                         Spanned spanned = (Spanned) text;
1544                         int spanEnd = spanned.nextSpanTransition(start, end,
1545                                 TabStopSpan.class);
1546                         TabStopSpan[] spans = getParagraphSpans(spanned, start, spanEnd,
1547                                 TabStopSpan.class);
1548                         if (spans.length > 0) {
1549                             tabStops = new TabStops(TAB_INCREMENT, spans);
1550                         }
1551                     }
1552                     break;
1553                 }
1554             }
1555             tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops);
1556             return tl.metrics(null);
1557         } finally {
1558             TextLine.recycle(tl);
1559             MeasuredText.recycle(mt);
1560         }
1561     }
1562 
1563     /**
1564      * @hide
1565      */
1566     /* package */ static class TabStops {
1567         private int[] mStops;
1568         private int mNumStops;
1569         private int mIncrement;
1570 
1571         TabStops(int increment, Object[] spans) {
1572             reset(increment, spans);
1573         }
1574 
1575         void reset(int increment, Object[] spans) {
1576             this.mIncrement = increment;
1577 
1578             int ns = 0;
1579             if (spans != null) {
1580                 int[] stops = this.mStops;
1581                 for (Object o : spans) {
1582                     if (o instanceof TabStopSpan) {
1583                         if (stops == null) {
1584                             stops = new int[10];
1585                         } else if (ns == stops.length) {
1586                             int[] nstops = new int[ns * 2];
1587                             for (int i = 0; i < ns; ++i) {
1588                                 nstops[i] = stops[i];
1589                             }
1590                             stops = nstops;
1591                         }
1592                         stops[ns++] = ((TabStopSpan) o).getTabStop();
1593                     }
1594                 }
1595                 if (ns > 1) {
1596                     Arrays.sort(stops, 0, ns);
1597                 }
1598                 if (stops != this.mStops) {
1599                     this.mStops = stops;
1600                 }
1601             }
1602             this.mNumStops = ns;
1603         }
1604 
1605         float nextTab(float h) {
1606             int ns = this.mNumStops;
1607             if (ns > 0) {
1608                 int[] stops = this.mStops;
1609                 for (int i = 0; i < ns; ++i) {
1610                     int stop = stops[i];
1611                     if (stop > h) {
1612                         return stop;
1613                     }
1614                 }
1615             }
1616             return nextDefaultStop(h, mIncrement);
1617         }
1618 
nextDefaultStop(float h, int inc)1619         public static float nextDefaultStop(float h, int inc) {
1620             return ((int) ((h + inc) / inc)) * inc;
1621         }
1622     }
1623 
1624     /**
1625      * Returns the position of the next tab stop after h on the line.
1626      *
1627      * @param text the text
1628      * @param start start of the line
1629      * @param end limit of the line
1630      * @param h the current horizontal offset
1631      * @param tabs the tabs, can be null.  If it is null, any tabs in effect
1632      * on the line will be used.  If there are no tabs, a default offset
1633      * will be used to compute the tab stop.
1634      * @return the offset of the next tab stop.
1635      */
nextTab(CharSequence text, int start, int end, float h, Object[] tabs)1636     /* package */ static float nextTab(CharSequence text, int start, int end,
1637                                        float h, Object[] tabs) {
1638         float nh = Float.MAX_VALUE;
1639         boolean alltabs = false;
1640 
1641         if (text instanceof Spanned) {
1642             if (tabs == null) {
1643                 tabs = getParagraphSpans((Spanned) text, start, end, TabStopSpan.class);
1644                 alltabs = true;
1645             }
1646 
1647             for (int i = 0; i < tabs.length; i++) {
1648                 if (!alltabs) {
1649                     if (!(tabs[i] instanceof TabStopSpan))
1650                         continue;
1651                 }
1652 
1653                 int where = ((TabStopSpan) tabs[i]).getTabStop();
1654 
1655                 if (where < nh && where > h)
1656                     nh = where;
1657             }
1658 
1659             if (nh != Float.MAX_VALUE)
1660                 return nh;
1661         }
1662 
1663         return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT;
1664     }
1665 
isSpanned()1666     protected final boolean isSpanned() {
1667         return mSpannedText;
1668     }
1669 
1670     /**
1671      * Returns the same as <code>text.getSpans()</code>, except where
1672      * <code>start</code> and <code>end</code> are the same and are not
1673      * at the very beginning of the text, in which case an empty array
1674      * is returned instead.
1675      * <p>
1676      * This is needed because of the special case that <code>getSpans()</code>
1677      * on an empty range returns the spans adjacent to that range, which is
1678      * primarily for the sake of <code>TextWatchers</code> so they will get
1679      * notifications when text goes from empty to non-empty.  But it also
1680      * has the unfortunate side effect that if the text ends with an empty
1681      * paragraph, that paragraph accidentally picks up the styles of the
1682      * preceding paragraph (even though those styles will not be picked up
1683      * by new text that is inserted into the empty paragraph).
1684      * <p>
1685      * The reason it just checks whether <code>start</code> and <code>end</code>
1686      * is the same is that the only time a line can contain 0 characters
1687      * is if it is the final paragraph of the Layout; otherwise any line will
1688      * contain at least one printing or newline character.  The reason for the
1689      * additional check if <code>start</code> is greater than 0 is that
1690      * if the empty paragraph is the entire content of the buffer, paragraph
1691      * styles that are already applied to the buffer will apply to text that
1692      * is inserted into it.
1693      */
getParagraphSpans(Spanned text, int start, int end, Class<T> type)1694     /* package */static <T> T[] getParagraphSpans(Spanned text, int start, int end, Class<T> type) {
1695         if (start == end && start > 0) {
1696             return ArrayUtils.emptyArray(type);
1697         }
1698 
1699         return text.getSpans(start, end, type);
1700     }
1701 
getEllipsisChar(TextUtils.TruncateAt method)1702     private char getEllipsisChar(TextUtils.TruncateAt method) {
1703         return (method == TextUtils.TruncateAt.END_SMALL) ?
1704                 ELLIPSIS_TWO_DOTS[0] :
1705                 ELLIPSIS_NORMAL[0];
1706     }
1707 
ellipsize(int start, int end, int line, char[] dest, int destoff, TextUtils.TruncateAt method)1708     private void ellipsize(int start, int end, int line,
1709                            char[] dest, int destoff, TextUtils.TruncateAt method) {
1710         int ellipsisCount = getEllipsisCount(line);
1711 
1712         if (ellipsisCount == 0) {
1713             return;
1714         }
1715 
1716         int ellipsisStart = getEllipsisStart(line);
1717         int linestart = getLineStart(line);
1718 
1719         for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) {
1720             char c;
1721 
1722             if (i == ellipsisStart) {
1723                 c = getEllipsisChar(method); // ellipsis
1724             } else {
1725                 c = '\uFEFF'; // 0-width space
1726             }
1727 
1728             int a = i + linestart;
1729 
1730             if (a >= start && a < end) {
1731                 dest[destoff + a - start] = c;
1732             }
1733         }
1734     }
1735 
1736     /**
1737      * Stores information about bidirectional (left-to-right or right-to-left)
1738      * text within the layout of a line.
1739      */
1740     public static class Directions {
1741         // Directions represents directional runs within a line of text.
1742         // Runs are pairs of ints listed in visual order, starting from the
1743         // leading margin.  The first int of each pair is the offset from
1744         // the first character of the line to the start of the run.  The
1745         // second int represents both the length and level of the run.
1746         // The length is in the lower bits, accessed by masking with
1747         // DIR_LENGTH_MASK.  The level is in the higher bits, accessed
1748         // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK.
1749         // To simply test for an RTL direction, test the bit using
1750         // DIR_RTL_FLAG, if set then the direction is rtl.
1751 
1752         /* package */ int[] mDirections;
Directions(int[] dirs)1753         /* package */ Directions(int[] dirs) {
1754             mDirections = dirs;
1755         }
1756     }
1757 
1758     /**
1759      * Return the offset of the first character to be ellipsized away,
1760      * relative to the start of the line.  (So 0 if the beginning of the
1761      * line is ellipsized, not getLineStart().)
1762      */
1763     public abstract int getEllipsisStart(int line);
1764 
1765     /**
1766      * Returns the number of characters to be ellipsized away, or 0 if
1767      * no ellipsis is to take place.
1768      */
1769     public abstract int getEllipsisCount(int line);
1770 
1771     /* package */ static class Ellipsizer implements CharSequence, GetChars {
1772         /* package */ CharSequence mText;
1773         /* package */ Layout mLayout;
1774         /* package */ int mWidth;
1775         /* package */ TextUtils.TruncateAt mMethod;
1776 
Ellipsizer(CharSequence s)1777         public Ellipsizer(CharSequence s) {
1778             mText = s;
1779         }
1780 
charAt(int off)1781         public char charAt(int off) {
1782             char[] buf = TextUtils.obtain(1);
1783             getChars(off, off + 1, buf, 0);
1784             char ret = buf[0];
1785 
1786             TextUtils.recycle(buf);
1787             return ret;
1788         }
1789 
getChars(int start, int end, char[] dest, int destoff)1790         public void getChars(int start, int end, char[] dest, int destoff) {
1791             int line1 = mLayout.getLineForOffset(start);
1792             int line2 = mLayout.getLineForOffset(end);
1793 
1794             TextUtils.getChars(mText, start, end, dest, destoff);
1795 
1796             for (int i = line1; i <= line2; i++) {
1797                 mLayout.ellipsize(start, end, i, dest, destoff, mMethod);
1798             }
1799         }
1800 
length()1801         public int length() {
1802             return mText.length();
1803         }
1804 
subSequence(int start, int end)1805         public CharSequence subSequence(int start, int end) {
1806             char[] s = new char[end - start];
1807             getChars(start, end, s, 0);
1808             return new String(s);
1809         }
1810 
1811         @Override
toString()1812         public String toString() {
1813             char[] s = new char[length()];
1814             getChars(0, length(), s, 0);
1815             return new String(s);
1816         }
1817 
1818     }
1819 
1820     /* package */ static class SpannedEllipsizer extends Ellipsizer implements Spanned {
1821         private Spanned mSpanned;
1822 
SpannedEllipsizer(CharSequence display)1823         public SpannedEllipsizer(CharSequence display) {
1824             super(display);
1825             mSpanned = (Spanned) display;
1826         }
1827 
getSpans(int start, int end, Class<T> type)1828         public <T> T[] getSpans(int start, int end, Class<T> type) {
1829             return mSpanned.getSpans(start, end, type);
1830         }
1831 
getSpanStart(Object tag)1832         public int getSpanStart(Object tag) {
1833             return mSpanned.getSpanStart(tag);
1834         }
1835 
getSpanEnd(Object tag)1836         public int getSpanEnd(Object tag) {
1837             return mSpanned.getSpanEnd(tag);
1838         }
1839 
getSpanFlags(Object tag)1840         public int getSpanFlags(Object tag) {
1841             return mSpanned.getSpanFlags(tag);
1842         }
1843 
1844         @SuppressWarnings("rawtypes")
nextSpanTransition(int start, int limit, Class type)1845         public int nextSpanTransition(int start, int limit, Class type) {
1846             return mSpanned.nextSpanTransition(start, limit, type);
1847         }
1848 
1849         @Override
subSequence(int start, int end)1850         public CharSequence subSequence(int start, int end) {
1851             char[] s = new char[end - start];
1852             getChars(start, end, s, 0);
1853 
1854             SpannableString ss = new SpannableString(new String(s));
1855             TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0);
1856             return ss;
1857         }
1858     }
1859 
1860     private CharSequence mText;
1861     private TextPaint mPaint;
1862     /* package */ TextPaint mWorkPaint;
1863     private int mWidth;
1864     private Alignment mAlignment = Alignment.ALIGN_NORMAL;
1865     private float mSpacingMult;
1866     private float mSpacingAdd;
1867     private static final Rect sTempRect = new Rect();
1868     private boolean mSpannedText;
1869     private TextDirectionHeuristic mTextDir;
1870     private SpanSet<LineBackgroundSpan> mLineBackgroundSpans;
1871 
1872     public static final int DIR_LEFT_TO_RIGHT = 1;
1873     public static final int DIR_RIGHT_TO_LEFT = -1;
1874 
1875     /* package */ static final int DIR_REQUEST_LTR = 1;
1876     /* package */ static final int DIR_REQUEST_RTL = -1;
1877     /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2;
1878     /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2;
1879 
1880     /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff;
1881     /* package */ static final int RUN_LEVEL_SHIFT = 26;
1882     /* package */ static final int RUN_LEVEL_MASK = 0x3f;
1883     /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT;
1884 
1885     public enum Alignment {
1886         ALIGN_NORMAL,
1887         ALIGN_OPPOSITE,
1888         ALIGN_CENTER,
1889         /** @hide */
1890         ALIGN_LEFT,
1891         /** @hide */
1892         ALIGN_RIGHT,
1893     }
1894 
1895     private static final int TAB_INCREMENT = 20;
1896 
1897     /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT =
1898         new Directions(new int[] { 0, RUN_LENGTH_MASK });
1899     /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT =
1900         new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG });
1901 
1902     /* package */ static final char[] ELLIPSIS_NORMAL = { '\u2026' }; // this is "..."
1903     /* package */ static final char[] ELLIPSIS_TWO_DOTS = { '\u2025' }; // this is ".."
1904 }
1905