• 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         return getPrimaryHorizontal(offset, false /* not clamped */);
796     }
797 
798     /**
799      * Get the primary horizontal position for the specified text offset, but
800      * optionally clamp it so that it doesn't exceed the width of the layout.
801      * @hide
802      */
getPrimaryHorizontal(int offset, boolean clamped)803     public float getPrimaryHorizontal(int offset, boolean clamped) {
804         boolean trailing = primaryIsTrailingPrevious(offset);
805         return getHorizontal(offset, trailing, clamped);
806     }
807 
808     /**
809      * Get the secondary horizontal position for the specified text offset.
810      * This is the location where a new character would be inserted in
811      * the direction other than the paragraph's primary direction.
812      */
getSecondaryHorizontal(int offset)813     public float getSecondaryHorizontal(int offset) {
814         return getSecondaryHorizontal(offset, false /* not clamped */);
815     }
816 
817     /**
818      * Get the secondary horizontal position for the specified text offset, but
819      * optionally clamp it so that it doesn't exceed the width of the layout.
820      * @hide
821      */
getSecondaryHorizontal(int offset, boolean clamped)822     public float getSecondaryHorizontal(int offset, boolean clamped) {
823         boolean trailing = primaryIsTrailingPrevious(offset);
824         return getHorizontal(offset, !trailing, clamped);
825     }
826 
getHorizontal(int offset, boolean trailing, boolean clamped)827     private float getHorizontal(int offset, boolean trailing, boolean clamped) {
828         int line = getLineForOffset(offset);
829 
830         return getHorizontal(offset, trailing, line, clamped);
831     }
832 
getHorizontal(int offset, boolean trailing, int line, boolean clamped)833     private float getHorizontal(int offset, boolean trailing, int line, boolean clamped) {
834         int start = getLineStart(line);
835         int end = getLineEnd(line);
836         int dir = getParagraphDirection(line);
837         boolean hasTabOrEmoji = getLineContainsTab(line);
838         Directions directions = getLineDirections(line);
839 
840         TabStops tabStops = null;
841         if (hasTabOrEmoji && mText instanceof Spanned) {
842             // Just checking this line should be good enough, tabs should be
843             // consistent across all lines in a paragraph.
844             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
845             if (tabs.length > 0) {
846                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
847             }
848         }
849 
850         TextLine tl = TextLine.obtain();
851         tl.set(mPaint, mText, start, end, dir, directions, hasTabOrEmoji, tabStops);
852         float wid = tl.measure(offset - start, trailing, null);
853         TextLine.recycle(tl);
854 
855         if (clamped && wid > mWidth) {
856             wid = mWidth;
857         }
858         int left = getParagraphLeft(line);
859         int right = getParagraphRight(line);
860 
861         return getLineStartPos(line, left, right) + wid;
862     }
863 
864     /**
865      * Get the leftmost position that should be exposed for horizontal
866      * scrolling on the specified line.
867      */
getLineLeft(int line)868     public float getLineLeft(int line) {
869         int dir = getParagraphDirection(line);
870         Alignment align = getParagraphAlignment(line);
871 
872         if (align == Alignment.ALIGN_LEFT) {
873             return 0;
874         } else if (align == Alignment.ALIGN_NORMAL) {
875             if (dir == DIR_RIGHT_TO_LEFT)
876                 return getParagraphRight(line) - getLineMax(line);
877             else
878                 return 0;
879         } else if (align == Alignment.ALIGN_RIGHT) {
880             return mWidth - getLineMax(line);
881         } else if (align == Alignment.ALIGN_OPPOSITE) {
882             if (dir == DIR_RIGHT_TO_LEFT)
883                 return 0;
884             else
885                 return mWidth - getLineMax(line);
886         } else { /* align == Alignment.ALIGN_CENTER */
887             int left = getParagraphLeft(line);
888             int right = getParagraphRight(line);
889             int max = ((int) getLineMax(line)) & ~1;
890 
891             return left + ((right - left) - max) / 2;
892         }
893     }
894 
895     /**
896      * Get the rightmost position that should be exposed for horizontal
897      * scrolling on the specified line.
898      */
getLineRight(int line)899     public float getLineRight(int line) {
900         int dir = getParagraphDirection(line);
901         Alignment align = getParagraphAlignment(line);
902 
903         if (align == Alignment.ALIGN_LEFT) {
904             return getParagraphLeft(line) + getLineMax(line);
905         } else if (align == Alignment.ALIGN_NORMAL) {
906             if (dir == DIR_RIGHT_TO_LEFT)
907                 return mWidth;
908             else
909                 return getParagraphLeft(line) + getLineMax(line);
910         } else if (align == Alignment.ALIGN_RIGHT) {
911             return mWidth;
912         } else if (align == Alignment.ALIGN_OPPOSITE) {
913             if (dir == DIR_RIGHT_TO_LEFT)
914                 return getLineMax(line);
915             else
916                 return mWidth;
917         } else { /* align == Alignment.ALIGN_CENTER */
918             int left = getParagraphLeft(line);
919             int right = getParagraphRight(line);
920             int max = ((int) getLineMax(line)) & ~1;
921 
922             return right - ((right - left) - max) / 2;
923         }
924     }
925 
926     /**
927      * Gets the unsigned horizontal extent of the specified line, including
928      * leading margin indent, but excluding trailing whitespace.
929      */
getLineMax(int line)930     public float getLineMax(int line) {
931         float margin = getParagraphLeadingMargin(line);
932         float signedExtent = getLineExtent(line, false);
933         return margin + signedExtent >= 0 ? signedExtent : -signedExtent;
934     }
935 
936     /**
937      * Gets the unsigned horizontal extent of the specified line, including
938      * leading margin indent and trailing whitespace.
939      */
getLineWidth(int line)940     public float getLineWidth(int line) {
941         float margin = getParagraphLeadingMargin(line);
942         float signedExtent = getLineExtent(line, true);
943         return margin + signedExtent >= 0 ? signedExtent : -signedExtent;
944     }
945 
946     /**
947      * Like {@link #getLineExtent(int,TabStops,boolean)} but determines the
948      * tab stops instead of using the ones passed in.
949      * @param line the index of the line
950      * @param full whether to include trailing whitespace
951      * @return the extent of the line
952      */
getLineExtent(int line, boolean full)953     private float getLineExtent(int line, boolean full) {
954         int start = getLineStart(line);
955         int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
956 
957         boolean hasTabsOrEmoji = getLineContainsTab(line);
958         TabStops tabStops = null;
959         if (hasTabsOrEmoji && mText instanceof Spanned) {
960             // Just checking this line should be good enough, tabs should be
961             // consistent across all lines in a paragraph.
962             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
963             if (tabs.length > 0) {
964                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
965             }
966         }
967         Directions directions = getLineDirections(line);
968         // Returned directions can actually be null
969         if (directions == null) {
970             return 0f;
971         }
972         int dir = getParagraphDirection(line);
973 
974         TextLine tl = TextLine.obtain();
975         tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops);
976         float width = tl.metrics(null);
977         TextLine.recycle(tl);
978         return width;
979     }
980 
981     /**
982      * Returns the signed horizontal extent of the specified line, excluding
983      * leading margin.  If full is false, excludes trailing whitespace.
984      * @param line the index of the line
985      * @param tabStops the tab stops, can be null if we know they're not used.
986      * @param full whether to include trailing whitespace
987      * @return the extent of the text on this line
988      */
getLineExtent(int line, TabStops tabStops, boolean full)989     private float getLineExtent(int line, TabStops tabStops, boolean full) {
990         int start = getLineStart(line);
991         int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
992         boolean hasTabsOrEmoji = getLineContainsTab(line);
993         Directions directions = getLineDirections(line);
994         int dir = getParagraphDirection(line);
995 
996         TextLine tl = TextLine.obtain();
997         tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops);
998         float width = tl.metrics(null);
999         TextLine.recycle(tl);
1000         return width;
1001     }
1002 
1003     /**
1004      * Get the line number corresponding to the specified vertical position.
1005      * If you ask for a position above 0, you get 0; if you ask for a position
1006      * below the bottom of the text, you get the last line.
1007      */
1008     // FIXME: It may be faster to do a linear search for layouts without many lines.
getLineForVertical(int vertical)1009     public int getLineForVertical(int vertical) {
1010         int high = getLineCount(), low = -1, guess;
1011 
1012         while (high - low > 1) {
1013             guess = (high + low) / 2;
1014 
1015             if (getLineTop(guess) > vertical)
1016                 high = guess;
1017             else
1018                 low = guess;
1019         }
1020 
1021         if (low < 0)
1022             return 0;
1023         else
1024             return low;
1025     }
1026 
1027     /**
1028      * Get the line number on which the specified text offset appears.
1029      * If you ask for a position before 0, you get 0; if you ask for a position
1030      * beyond the end of the text, you get the last line.
1031      */
getLineForOffset(int offset)1032     public int getLineForOffset(int offset) {
1033         int high = getLineCount(), low = -1, guess;
1034 
1035         while (high - low > 1) {
1036             guess = (high + low) / 2;
1037 
1038             if (getLineStart(guess) > offset)
1039                 high = guess;
1040             else
1041                 low = guess;
1042         }
1043 
1044         if (low < 0)
1045             return 0;
1046         else
1047             return low;
1048     }
1049 
1050     /**
1051      * Get the character offset on the specified line whose position is
1052      * closest to the specified horizontal position.
1053      */
getOffsetForHorizontal(int line, float horiz)1054     public int getOffsetForHorizontal(int line, float horiz) {
1055         int max = getLineEnd(line) - 1;
1056         int min = getLineStart(line);
1057         Directions dirs = getLineDirections(line);
1058 
1059         if (line == getLineCount() - 1)
1060             max++;
1061 
1062         int best = min;
1063         float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz);
1064 
1065         for (int i = 0; i < dirs.mDirections.length; i += 2) {
1066             int here = min + dirs.mDirections[i];
1067             int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK);
1068             int swap = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0 ? -1 : 1;
1069 
1070             if (there > max)
1071                 there = max;
1072             int high = there - 1 + 1, low = here + 1 - 1, guess;
1073 
1074             while (high - low > 1) {
1075                 guess = (high + low) / 2;
1076                 int adguess = getOffsetAtStartOf(guess);
1077 
1078                 if (getPrimaryHorizontal(adguess) * swap >= horiz * swap)
1079                     high = guess;
1080                 else
1081                     low = guess;
1082             }
1083 
1084             if (low < here + 1)
1085                 low = here + 1;
1086 
1087             if (low < there) {
1088                 low = getOffsetAtStartOf(low);
1089 
1090                 float dist = Math.abs(getPrimaryHorizontal(low) - horiz);
1091 
1092                 int aft = TextUtils.getOffsetAfter(mText, low);
1093                 if (aft < there) {
1094                     float other = Math.abs(getPrimaryHorizontal(aft) - horiz);
1095 
1096                     if (other < dist) {
1097                         dist = other;
1098                         low = aft;
1099                     }
1100                 }
1101 
1102                 if (dist < bestdist) {
1103                     bestdist = dist;
1104                     best = low;
1105                 }
1106             }
1107 
1108             float dist = Math.abs(getPrimaryHorizontal(here) - horiz);
1109 
1110             if (dist < bestdist) {
1111                 bestdist = dist;
1112                 best = here;
1113             }
1114         }
1115 
1116         float dist = Math.abs(getPrimaryHorizontal(max) - horiz);
1117 
1118         if (dist <= bestdist) {
1119             bestdist = dist;
1120             best = max;
1121         }
1122 
1123         return best;
1124     }
1125 
1126     /**
1127      * Return the text offset after the last character on the specified line.
1128      */
getLineEnd(int line)1129     public final int getLineEnd(int line) {
1130         return getLineStart(line + 1);
1131     }
1132 
1133     /**
1134      * Return the text offset after the last visible character (so whitespace
1135      * is not counted) on the specified line.
1136      */
getLineVisibleEnd(int line)1137     public int getLineVisibleEnd(int line) {
1138         return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1));
1139     }
1140 
getLineVisibleEnd(int line, int start, int end)1141     private int getLineVisibleEnd(int line, int start, int end) {
1142         CharSequence text = mText;
1143         char ch;
1144         if (line == getLineCount() - 1) {
1145             return end;
1146         }
1147 
1148         for (; end > start; end--) {
1149             ch = text.charAt(end - 1);
1150 
1151             if (ch == '\n') {
1152                 return end - 1;
1153             }
1154 
1155             if (ch != ' ' && ch != '\t') {
1156                 break;
1157             }
1158 
1159         }
1160 
1161         return end;
1162     }
1163 
1164     /**
1165      * Return the vertical position of the bottom of the specified line.
1166      */
getLineBottom(int line)1167     public final int getLineBottom(int line) {
1168         return getLineTop(line + 1);
1169     }
1170 
1171     /**
1172      * Return the vertical position of the baseline of the specified line.
1173      */
getLineBaseline(int line)1174     public final int getLineBaseline(int line) {
1175         // getLineTop(line+1) == getLineTop(line)
1176         return getLineTop(line+1) - getLineDescent(line);
1177     }
1178 
1179     /**
1180      * Get the ascent of the text on the specified line.
1181      * The return value is negative to match the Paint.ascent() convention.
1182      */
getLineAscent(int line)1183     public final int getLineAscent(int line) {
1184         // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line)
1185         return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line));
1186     }
1187 
getOffsetToLeftOf(int offset)1188     public int getOffsetToLeftOf(int offset) {
1189         return getOffsetToLeftRightOf(offset, true);
1190     }
1191 
getOffsetToRightOf(int offset)1192     public int getOffsetToRightOf(int offset) {
1193         return getOffsetToLeftRightOf(offset, false);
1194     }
1195 
getOffsetToLeftRightOf(int caret, boolean toLeft)1196     private int getOffsetToLeftRightOf(int caret, boolean toLeft) {
1197         int line = getLineForOffset(caret);
1198         int lineStart = getLineStart(line);
1199         int lineEnd = getLineEnd(line);
1200         int lineDir = getParagraphDirection(line);
1201 
1202         boolean lineChanged = false;
1203         boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT);
1204         // if walking off line, look at the line we're headed to
1205         if (advance) {
1206             if (caret == lineEnd) {
1207                 if (line < getLineCount() - 1) {
1208                     lineChanged = true;
1209                     ++line;
1210                 } else {
1211                     return caret; // at very end, don't move
1212                 }
1213             }
1214         } else {
1215             if (caret == lineStart) {
1216                 if (line > 0) {
1217                     lineChanged = true;
1218                     --line;
1219                 } else {
1220                     return caret; // at very start, don't move
1221                 }
1222             }
1223         }
1224 
1225         if (lineChanged) {
1226             lineStart = getLineStart(line);
1227             lineEnd = getLineEnd(line);
1228             int newDir = getParagraphDirection(line);
1229             if (newDir != lineDir) {
1230                 // unusual case.  we want to walk onto the line, but it runs
1231                 // in a different direction than this one, so we fake movement
1232                 // in the opposite direction.
1233                 toLeft = !toLeft;
1234                 lineDir = newDir;
1235             }
1236         }
1237 
1238         Directions directions = getLineDirections(line);
1239 
1240         TextLine tl = TextLine.obtain();
1241         // XXX: we don't care about tabs
1242         tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null);
1243         caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft);
1244         tl = TextLine.recycle(tl);
1245         return caret;
1246     }
1247 
getOffsetAtStartOf(int offset)1248     private int getOffsetAtStartOf(int offset) {
1249         // XXX this probably should skip local reorderings and
1250         // zero-width characters, look at callers
1251         if (offset == 0)
1252             return 0;
1253 
1254         CharSequence text = mText;
1255         char c = text.charAt(offset);
1256 
1257         if (c >= '\uDC00' && c <= '\uDFFF') {
1258             char c1 = text.charAt(offset - 1);
1259 
1260             if (c1 >= '\uD800' && c1 <= '\uDBFF')
1261                 offset -= 1;
1262         }
1263 
1264         if (mSpannedText) {
1265             ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
1266                                                        ReplacementSpan.class);
1267 
1268             for (int i = 0; i < spans.length; i++) {
1269                 int start = ((Spanned) text).getSpanStart(spans[i]);
1270                 int end = ((Spanned) text).getSpanEnd(spans[i]);
1271 
1272                 if (start < offset && end > offset)
1273                     offset = start;
1274             }
1275         }
1276 
1277         return offset;
1278     }
1279 
1280     /**
1281      * Determine whether we should clamp cursor position. Currently it's
1282      * only robust for left-aligned displays.
1283      * @hide
1284      */
shouldClampCursor(int line)1285     public boolean shouldClampCursor(int line) {
1286         // Only clamp cursor position in left-aligned displays.
1287         switch (getParagraphAlignment(line)) {
1288             case ALIGN_LEFT:
1289                 return true;
1290             case ALIGN_NORMAL:
1291                 return getParagraphDirection(line) > 0;
1292             default:
1293                 return false;
1294         }
1295 
1296     }
1297     /**
1298      * Fills in the specified Path with a representation of a cursor
1299      * at the specified offset.  This will often be a vertical line
1300      * but can be multiple discontinuous lines in text with multiple
1301      * directionalities.
1302      */
getCursorPath(int point, Path dest, CharSequence editingBuffer)1303     public void getCursorPath(int point, Path dest,
1304                               CharSequence editingBuffer) {
1305         dest.reset();
1306 
1307         int line = getLineForOffset(point);
1308         int top = getLineTop(line);
1309         int bottom = getLineTop(line+1);
1310 
1311         boolean clamped = shouldClampCursor(line);
1312         float h1 = getPrimaryHorizontal(point, clamped) - 0.5f;
1313         float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point, clamped) - 0.5f : h1;
1314 
1315         int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) |
1316                    TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING);
1317         int fn = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_ALT_ON);
1318         int dist = 0;
1319 
1320         if (caps != 0 || fn != 0) {
1321             dist = (bottom - top) >> 2;
1322 
1323             if (fn != 0)
1324                 top += dist;
1325             if (caps != 0)
1326                 bottom -= dist;
1327         }
1328 
1329         if (h1 < 0.5f)
1330             h1 = 0.5f;
1331         if (h2 < 0.5f)
1332             h2 = 0.5f;
1333 
1334         if (Float.compare(h1, h2) == 0) {
1335             dest.moveTo(h1, top);
1336             dest.lineTo(h1, bottom);
1337         } else {
1338             dest.moveTo(h1, top);
1339             dest.lineTo(h1, (top + bottom) >> 1);
1340 
1341             dest.moveTo(h2, (top + bottom) >> 1);
1342             dest.lineTo(h2, bottom);
1343         }
1344 
1345         if (caps == 2) {
1346             dest.moveTo(h2, bottom);
1347             dest.lineTo(h2 - dist, bottom + dist);
1348             dest.lineTo(h2, bottom);
1349             dest.lineTo(h2 + dist, bottom + dist);
1350         } else if (caps == 1) {
1351             dest.moveTo(h2, bottom);
1352             dest.lineTo(h2 - dist, bottom + dist);
1353 
1354             dest.moveTo(h2 - dist, bottom + dist - 0.5f);
1355             dest.lineTo(h2 + dist, bottom + dist - 0.5f);
1356 
1357             dest.moveTo(h2 + dist, bottom + dist);
1358             dest.lineTo(h2, bottom);
1359         }
1360 
1361         if (fn == 2) {
1362             dest.moveTo(h1, top);
1363             dest.lineTo(h1 - dist, top - dist);
1364             dest.lineTo(h1, top);
1365             dest.lineTo(h1 + dist, top - dist);
1366         } else if (fn == 1) {
1367             dest.moveTo(h1, top);
1368             dest.lineTo(h1 - dist, top - dist);
1369 
1370             dest.moveTo(h1 - dist, top - dist + 0.5f);
1371             dest.lineTo(h1 + dist, top - dist + 0.5f);
1372 
1373             dest.moveTo(h1 + dist, top - dist);
1374             dest.lineTo(h1, top);
1375         }
1376     }
1377 
addSelection(int line, int start, int end, int top, int bottom, Path dest)1378     private void addSelection(int line, int start, int end,
1379                               int top, int bottom, Path dest) {
1380         int linestart = getLineStart(line);
1381         int lineend = getLineEnd(line);
1382         Directions dirs = getLineDirections(line);
1383 
1384         if (lineend > linestart && mText.charAt(lineend - 1) == '\n')
1385             lineend--;
1386 
1387         for (int i = 0; i < dirs.mDirections.length; i += 2) {
1388             int here = linestart + dirs.mDirections[i];
1389             int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK);
1390 
1391             if (there > lineend)
1392                 there = lineend;
1393 
1394             if (start <= there && end >= here) {
1395                 int st = Math.max(start, here);
1396                 int en = Math.min(end, there);
1397 
1398                 if (st != en) {
1399                     float h1 = getHorizontal(st, false, line, false /* not clamped */);
1400                     float h2 = getHorizontal(en, true, line, false /* not clamped */);
1401 
1402                     float left = Math.min(h1, h2);
1403                     float right = Math.max(h1, h2);
1404 
1405                     dest.addRect(left, top, right, bottom, Path.Direction.CW);
1406                 }
1407             }
1408         }
1409     }
1410 
1411     /**
1412      * Fills in the specified Path with a representation of a highlight
1413      * between the specified offsets.  This will often be a rectangle
1414      * or a potentially discontinuous set of rectangles.  If the start
1415      * and end are the same, the returned path is empty.
1416      */
getSelectionPath(int start, int end, Path dest)1417     public void getSelectionPath(int start, int end, Path dest) {
1418         dest.reset();
1419 
1420         if (start == end)
1421             return;
1422 
1423         if (end < start) {
1424             int temp = end;
1425             end = start;
1426             start = temp;
1427         }
1428 
1429         int startline = getLineForOffset(start);
1430         int endline = getLineForOffset(end);
1431 
1432         int top = getLineTop(startline);
1433         int bottom = getLineBottom(endline);
1434 
1435         if (startline == endline) {
1436             addSelection(startline, start, end, top, bottom, dest);
1437         } else {
1438             final float width = mWidth;
1439 
1440             addSelection(startline, start, getLineEnd(startline),
1441                          top, getLineBottom(startline), dest);
1442 
1443             if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT)
1444                 dest.addRect(getLineLeft(startline), top,
1445                               0, getLineBottom(startline), Path.Direction.CW);
1446             else
1447                 dest.addRect(getLineRight(startline), top,
1448                               width, getLineBottom(startline), Path.Direction.CW);
1449 
1450             for (int i = startline + 1; i < endline; i++) {
1451                 top = getLineTop(i);
1452                 bottom = getLineBottom(i);
1453                 dest.addRect(0, top, width, bottom, Path.Direction.CW);
1454             }
1455 
1456             top = getLineTop(endline);
1457             bottom = getLineBottom(endline);
1458 
1459             addSelection(endline, getLineStart(endline), end,
1460                          top, bottom, dest);
1461 
1462             if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT)
1463                 dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW);
1464             else
1465                 dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW);
1466         }
1467     }
1468 
1469     /**
1470      * Get the alignment of the specified paragraph, taking into account
1471      * markup attached to it.
1472      */
getParagraphAlignment(int line)1473     public final Alignment getParagraphAlignment(int line) {
1474         Alignment align = mAlignment;
1475 
1476         if (mSpannedText) {
1477             Spanned sp = (Spanned) mText;
1478             AlignmentSpan[] spans = getParagraphSpans(sp, getLineStart(line),
1479                                                 getLineEnd(line),
1480                                                 AlignmentSpan.class);
1481 
1482             int spanLength = spans.length;
1483             if (spanLength > 0) {
1484                 align = spans[spanLength-1].getAlignment();
1485             }
1486         }
1487 
1488         return align;
1489     }
1490 
1491     /**
1492      * Get the left edge of the specified paragraph, inset by left margins.
1493      */
getParagraphLeft(int line)1494     public final int getParagraphLeft(int line) {
1495         int left = 0;
1496         int dir = getParagraphDirection(line);
1497         if (dir == DIR_RIGHT_TO_LEFT || !mSpannedText) {
1498             return left; // leading margin has no impact, or no styles
1499         }
1500         return getParagraphLeadingMargin(line);
1501     }
1502 
1503     /**
1504      * Get the right edge of the specified paragraph, inset by right margins.
1505      */
getParagraphRight(int line)1506     public final int getParagraphRight(int line) {
1507         int right = mWidth;
1508         int dir = getParagraphDirection(line);
1509         if (dir == DIR_LEFT_TO_RIGHT || !mSpannedText) {
1510             return right; // leading margin has no impact, or no styles
1511         }
1512         return right - getParagraphLeadingMargin(line);
1513     }
1514 
1515     /**
1516      * Returns the effective leading margin (unsigned) for this line,
1517      * taking into account LeadingMarginSpan and LeadingMarginSpan2.
1518      * @param line the line index
1519      * @return the leading margin of this line
1520      */
getParagraphLeadingMargin(int line)1521     private int getParagraphLeadingMargin(int line) {
1522         if (!mSpannedText) {
1523             return 0;
1524         }
1525         Spanned spanned = (Spanned) mText;
1526 
1527         int lineStart = getLineStart(line);
1528         int lineEnd = getLineEnd(line);
1529         int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd,
1530                 LeadingMarginSpan.class);
1531         LeadingMarginSpan[] spans = getParagraphSpans(spanned, lineStart, spanEnd,
1532                                                 LeadingMarginSpan.class);
1533         if (spans.length == 0) {
1534             return 0; // no leading margin span;
1535         }
1536 
1537         int margin = 0;
1538 
1539         boolean isFirstParaLine = lineStart == 0 ||
1540             spanned.charAt(lineStart - 1) == '\n';
1541 
1542         for (int i = 0; i < spans.length; i++) {
1543             LeadingMarginSpan span = spans[i];
1544             boolean useFirstLineMargin = isFirstParaLine;
1545             if (span instanceof LeadingMarginSpan2) {
1546                 int spStart = spanned.getSpanStart(span);
1547                 int spanLine = getLineForOffset(spStart);
1548                 int count = ((LeadingMarginSpan2)span).getLeadingMarginLineCount();
1549                 useFirstLineMargin = line < spanLine + count;
1550             }
1551             margin += span.getLeadingMargin(useFirstLineMargin);
1552         }
1553 
1554         return margin;
1555     }
1556 
1557     /* package */
1558     static float measurePara(TextPaint paint, CharSequence text, int start, int end) {
1559 
1560         MeasuredText mt = MeasuredText.obtain();
1561         TextLine tl = TextLine.obtain();
1562         try {
1563             mt.setPara(text, start, end, TextDirectionHeuristics.LTR);
1564             Directions directions;
1565             int dir;
1566             if (mt.mEasy) {
1567                 directions = DIRS_ALL_LEFT_TO_RIGHT;
1568                 dir = Layout.DIR_LEFT_TO_RIGHT;
1569             } else {
1570                 directions = AndroidBidi.directions(mt.mDir, mt.mLevels,
1571                     0, mt.mChars, 0, mt.mLen);
1572                 dir = mt.mDir;
1573             }
1574             char[] chars = mt.mChars;
1575             int len = mt.mLen;
1576             boolean hasTabs = false;
1577             TabStops tabStops = null;
1578             for (int i = 0; i < len; ++i) {
1579                 if (chars[i] == '\t') {
1580                     hasTabs = true;
1581                     if (text instanceof Spanned) {
1582                         Spanned spanned = (Spanned) text;
1583                         int spanEnd = spanned.nextSpanTransition(start, end,
1584                                 TabStopSpan.class);
1585                         TabStopSpan[] spans = getParagraphSpans(spanned, start, spanEnd,
1586                                 TabStopSpan.class);
1587                         if (spans.length > 0) {
1588                             tabStops = new TabStops(TAB_INCREMENT, spans);
1589                         }
1590                     }
1591                     break;
1592                 }
1593             }
1594             tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops);
1595             return tl.metrics(null);
1596         } finally {
1597             TextLine.recycle(tl);
1598             MeasuredText.recycle(mt);
1599         }
1600     }
1601 
1602     /**
1603      * @hide
1604      */
1605     /* package */ static class TabStops {
1606         private int[] mStops;
1607         private int mNumStops;
1608         private int mIncrement;
1609 
1610         TabStops(int increment, Object[] spans) {
1611             reset(increment, spans);
1612         }
1613 
1614         void reset(int increment, Object[] spans) {
1615             this.mIncrement = increment;
1616 
1617             int ns = 0;
1618             if (spans != null) {
1619                 int[] stops = this.mStops;
1620                 for (Object o : spans) {
1621                     if (o instanceof TabStopSpan) {
1622                         if (stops == null) {
1623                             stops = new int[10];
1624                         } else if (ns == stops.length) {
1625                             int[] nstops = new int[ns * 2];
1626                             for (int i = 0; i < ns; ++i) {
1627                                 nstops[i] = stops[i];
1628                             }
1629                             stops = nstops;
1630                         }
1631                         stops[ns++] = ((TabStopSpan) o).getTabStop();
1632                     }
1633                 }
1634                 if (ns > 1) {
1635                     Arrays.sort(stops, 0, ns);
1636                 }
1637                 if (stops != this.mStops) {
1638                     this.mStops = stops;
1639                 }
1640             }
1641             this.mNumStops = ns;
1642         }
1643 
1644         float nextTab(float h) {
1645             int ns = this.mNumStops;
1646             if (ns > 0) {
1647                 int[] stops = this.mStops;
1648                 for (int i = 0; i < ns; ++i) {
1649                     int stop = stops[i];
1650                     if (stop > h) {
1651                         return stop;
1652                     }
1653                 }
1654             }
1655             return nextDefaultStop(h, mIncrement);
1656         }
1657 
nextDefaultStop(float h, int inc)1658         public static float nextDefaultStop(float h, int inc) {
1659             return ((int) ((h + inc) / inc)) * inc;
1660         }
1661     }
1662 
1663     /**
1664      * Returns the position of the next tab stop after h on the line.
1665      *
1666      * @param text the text
1667      * @param start start of the line
1668      * @param end limit of the line
1669      * @param h the current horizontal offset
1670      * @param tabs the tabs, can be null.  If it is null, any tabs in effect
1671      * on the line will be used.  If there are no tabs, a default offset
1672      * will be used to compute the tab stop.
1673      * @return the offset of the next tab stop.
1674      */
nextTab(CharSequence text, int start, int end, float h, Object[] tabs)1675     /* package */ static float nextTab(CharSequence text, int start, int end,
1676                                        float h, Object[] tabs) {
1677         float nh = Float.MAX_VALUE;
1678         boolean alltabs = false;
1679 
1680         if (text instanceof Spanned) {
1681             if (tabs == null) {
1682                 tabs = getParagraphSpans((Spanned) text, start, end, TabStopSpan.class);
1683                 alltabs = true;
1684             }
1685 
1686             for (int i = 0; i < tabs.length; i++) {
1687                 if (!alltabs) {
1688                     if (!(tabs[i] instanceof TabStopSpan))
1689                         continue;
1690                 }
1691 
1692                 int where = ((TabStopSpan) tabs[i]).getTabStop();
1693 
1694                 if (where < nh && where > h)
1695                     nh = where;
1696             }
1697 
1698             if (nh != Float.MAX_VALUE)
1699                 return nh;
1700         }
1701 
1702         return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT;
1703     }
1704 
isSpanned()1705     protected final boolean isSpanned() {
1706         return mSpannedText;
1707     }
1708 
1709     /**
1710      * Returns the same as <code>text.getSpans()</code>, except where
1711      * <code>start</code> and <code>end</code> are the same and are not
1712      * at the very beginning of the text, in which case an empty array
1713      * is returned instead.
1714      * <p>
1715      * This is needed because of the special case that <code>getSpans()</code>
1716      * on an empty range returns the spans adjacent to that range, which is
1717      * primarily for the sake of <code>TextWatchers</code> so they will get
1718      * notifications when text goes from empty to non-empty.  But it also
1719      * has the unfortunate side effect that if the text ends with an empty
1720      * paragraph, that paragraph accidentally picks up the styles of the
1721      * preceding paragraph (even though those styles will not be picked up
1722      * by new text that is inserted into the empty paragraph).
1723      * <p>
1724      * The reason it just checks whether <code>start</code> and <code>end</code>
1725      * is the same is that the only time a line can contain 0 characters
1726      * is if it is the final paragraph of the Layout; otherwise any line will
1727      * contain at least one printing or newline character.  The reason for the
1728      * additional check if <code>start</code> is greater than 0 is that
1729      * if the empty paragraph is the entire content of the buffer, paragraph
1730      * styles that are already applied to the buffer will apply to text that
1731      * is inserted into it.
1732      */
getParagraphSpans(Spanned text, int start, int end, Class<T> type)1733     /* package */static <T> T[] getParagraphSpans(Spanned text, int start, int end, Class<T> type) {
1734         if (start == end && start > 0) {
1735             return ArrayUtils.emptyArray(type);
1736         }
1737 
1738         return text.getSpans(start, end, type);
1739     }
1740 
getEllipsisChar(TextUtils.TruncateAt method)1741     private char getEllipsisChar(TextUtils.TruncateAt method) {
1742         return (method == TextUtils.TruncateAt.END_SMALL) ?
1743                 ELLIPSIS_TWO_DOTS[0] :
1744                 ELLIPSIS_NORMAL[0];
1745     }
1746 
ellipsize(int start, int end, int line, char[] dest, int destoff, TextUtils.TruncateAt method)1747     private void ellipsize(int start, int end, int line,
1748                            char[] dest, int destoff, TextUtils.TruncateAt method) {
1749         int ellipsisCount = getEllipsisCount(line);
1750 
1751         if (ellipsisCount == 0) {
1752             return;
1753         }
1754 
1755         int ellipsisStart = getEllipsisStart(line);
1756         int linestart = getLineStart(line);
1757 
1758         for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) {
1759             char c;
1760 
1761             if (i == ellipsisStart) {
1762                 c = getEllipsisChar(method); // ellipsis
1763             } else {
1764                 c = '\uFEFF'; // 0-width space
1765             }
1766 
1767             int a = i + linestart;
1768 
1769             if (a >= start && a < end) {
1770                 dest[destoff + a - start] = c;
1771             }
1772         }
1773     }
1774 
1775     /**
1776      * Stores information about bidirectional (left-to-right or right-to-left)
1777      * text within the layout of a line.
1778      */
1779     public static class Directions {
1780         // Directions represents directional runs within a line of text.
1781         // Runs are pairs of ints listed in visual order, starting from the
1782         // leading margin.  The first int of each pair is the offset from
1783         // the first character of the line to the start of the run.  The
1784         // second int represents both the length and level of the run.
1785         // The length is in the lower bits, accessed by masking with
1786         // DIR_LENGTH_MASK.  The level is in the higher bits, accessed
1787         // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK.
1788         // To simply test for an RTL direction, test the bit using
1789         // DIR_RTL_FLAG, if set then the direction is rtl.
1790 
1791         /* package */ int[] mDirections;
Directions(int[] dirs)1792         /* package */ Directions(int[] dirs) {
1793             mDirections = dirs;
1794         }
1795     }
1796 
1797     /**
1798      * Return the offset of the first character to be ellipsized away,
1799      * relative to the start of the line.  (So 0 if the beginning of the
1800      * line is ellipsized, not getLineStart().)
1801      */
1802     public abstract int getEllipsisStart(int line);
1803 
1804     /**
1805      * Returns the number of characters to be ellipsized away, or 0 if
1806      * no ellipsis is to take place.
1807      */
1808     public abstract int getEllipsisCount(int line);
1809 
1810     /* package */ static class Ellipsizer implements CharSequence, GetChars {
1811         /* package */ CharSequence mText;
1812         /* package */ Layout mLayout;
1813         /* package */ int mWidth;
1814         /* package */ TextUtils.TruncateAt mMethod;
1815 
Ellipsizer(CharSequence s)1816         public Ellipsizer(CharSequence s) {
1817             mText = s;
1818         }
1819 
charAt(int off)1820         public char charAt(int off) {
1821             char[] buf = TextUtils.obtain(1);
1822             getChars(off, off + 1, buf, 0);
1823             char ret = buf[0];
1824 
1825             TextUtils.recycle(buf);
1826             return ret;
1827         }
1828 
getChars(int start, int end, char[] dest, int destoff)1829         public void getChars(int start, int end, char[] dest, int destoff) {
1830             int line1 = mLayout.getLineForOffset(start);
1831             int line2 = mLayout.getLineForOffset(end);
1832 
1833             TextUtils.getChars(mText, start, end, dest, destoff);
1834 
1835             for (int i = line1; i <= line2; i++) {
1836                 mLayout.ellipsize(start, end, i, dest, destoff, mMethod);
1837             }
1838         }
1839 
length()1840         public int length() {
1841             return mText.length();
1842         }
1843 
subSequence(int start, int end)1844         public CharSequence subSequence(int start, int end) {
1845             char[] s = new char[end - start];
1846             getChars(start, end, s, 0);
1847             return new String(s);
1848         }
1849 
1850         @Override
toString()1851         public String toString() {
1852             char[] s = new char[length()];
1853             getChars(0, length(), s, 0);
1854             return new String(s);
1855         }
1856 
1857     }
1858 
1859     /* package */ static class SpannedEllipsizer extends Ellipsizer implements Spanned {
1860         private Spanned mSpanned;
1861 
SpannedEllipsizer(CharSequence display)1862         public SpannedEllipsizer(CharSequence display) {
1863             super(display);
1864             mSpanned = (Spanned) display;
1865         }
1866 
getSpans(int start, int end, Class<T> type)1867         public <T> T[] getSpans(int start, int end, Class<T> type) {
1868             return mSpanned.getSpans(start, end, type);
1869         }
1870 
getSpanStart(Object tag)1871         public int getSpanStart(Object tag) {
1872             return mSpanned.getSpanStart(tag);
1873         }
1874 
getSpanEnd(Object tag)1875         public int getSpanEnd(Object tag) {
1876             return mSpanned.getSpanEnd(tag);
1877         }
1878 
getSpanFlags(Object tag)1879         public int getSpanFlags(Object tag) {
1880             return mSpanned.getSpanFlags(tag);
1881         }
1882 
1883         @SuppressWarnings("rawtypes")
nextSpanTransition(int start, int limit, Class type)1884         public int nextSpanTransition(int start, int limit, Class type) {
1885             return mSpanned.nextSpanTransition(start, limit, type);
1886         }
1887 
1888         @Override
subSequence(int start, int end)1889         public CharSequence subSequence(int start, int end) {
1890             char[] s = new char[end - start];
1891             getChars(start, end, s, 0);
1892 
1893             SpannableString ss = new SpannableString(new String(s));
1894             TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0);
1895             return ss;
1896         }
1897     }
1898 
1899     private CharSequence mText;
1900     private TextPaint mPaint;
1901     /* package */ TextPaint mWorkPaint;
1902     private int mWidth;
1903     private Alignment mAlignment = Alignment.ALIGN_NORMAL;
1904     private float mSpacingMult;
1905     private float mSpacingAdd;
1906     private static final Rect sTempRect = new Rect();
1907     private boolean mSpannedText;
1908     private TextDirectionHeuristic mTextDir;
1909     private SpanSet<LineBackgroundSpan> mLineBackgroundSpans;
1910 
1911     public static final int DIR_LEFT_TO_RIGHT = 1;
1912     public static final int DIR_RIGHT_TO_LEFT = -1;
1913 
1914     /* package */ static final int DIR_REQUEST_LTR = 1;
1915     /* package */ static final int DIR_REQUEST_RTL = -1;
1916     /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2;
1917     /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2;
1918 
1919     /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff;
1920     /* package */ static final int RUN_LEVEL_SHIFT = 26;
1921     /* package */ static final int RUN_LEVEL_MASK = 0x3f;
1922     /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT;
1923 
1924     public enum Alignment {
1925         ALIGN_NORMAL,
1926         ALIGN_OPPOSITE,
1927         ALIGN_CENTER,
1928         /** @hide */
1929         ALIGN_LEFT,
1930         /** @hide */
1931         ALIGN_RIGHT,
1932     }
1933 
1934     private static final int TAB_INCREMENT = 20;
1935 
1936     /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT =
1937         new Directions(new int[] { 0, RUN_LENGTH_MASK });
1938     /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT =
1939         new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG });
1940 
1941     /* package */ static final char[] ELLIPSIS_NORMAL = { '\u2026' }; // this is "..."
1942     /* package */ static final char[] ELLIPSIS_TWO_DOTS = { '\u2025' }; // this is ".."
1943 }
1944