• 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         int x;
479         if (align == Alignment.ALIGN_LEFT) {
480             x = left;
481         } else if (align == Alignment.ALIGN_NORMAL) {
482             if (dir == DIR_LEFT_TO_RIGHT) {
483                 x = left;
484             } else {
485                 x = right;
486             }
487         } else {
488             TabStops tabStops = null;
489             if (mSpannedText && getLineContainsTab(line)) {
490                 Spanned spanned = (Spanned) mText;
491                 int start = getLineStart(line);
492                 int spanEnd = spanned.nextSpanTransition(start, spanned.length(),
493                         TabStopSpan.class);
494                 TabStopSpan[] tabSpans = getParagraphSpans(spanned, start, spanEnd,
495                         TabStopSpan.class);
496                 if (tabSpans.length > 0) {
497                     tabStops = new TabStops(TAB_INCREMENT, tabSpans);
498                 }
499             }
500             int max = (int)getLineExtent(line, tabStops, false);
501             if (align == Alignment.ALIGN_RIGHT) {
502                 x = right - max;
503             } else if (align == Alignment.ALIGN_OPPOSITE) {
504                 if (dir == DIR_LEFT_TO_RIGHT) {
505                     x = right - max;
506                 } else {
507                     x = left - max;
508                 }
509             } else { // Alignment.ALIGN_CENTER
510                 max = max & ~1;
511                 x = (left + right - max) >> 1;
512             }
513         }
514         return x;
515     }
516 
517     /**
518      * Return the text that is displayed by this Layout.
519      */
getText()520     public final CharSequence getText() {
521         return mText;
522     }
523 
524     /**
525      * Return the base Paint properties for this layout.
526      * Do NOT change the paint, which may result in funny
527      * drawing for this layout.
528      */
getPaint()529     public final TextPaint getPaint() {
530         return mPaint;
531     }
532 
533     /**
534      * Return the width of this layout.
535      */
getWidth()536     public final int getWidth() {
537         return mWidth;
538     }
539 
540     /**
541      * Return the width to which this Layout is ellipsizing, or
542      * {@link #getWidth} if it is not doing anything special.
543      */
getEllipsizedWidth()544     public int getEllipsizedWidth() {
545         return mWidth;
546     }
547 
548     /**
549      * Increase the width of this layout to the specified width.
550      * Be careful to use this only when you know it is appropriate&mdash;
551      * it does not cause the text to reflow to use the full new width.
552      */
increaseWidthTo(int wid)553     public final void increaseWidthTo(int wid) {
554         if (wid < mWidth) {
555             throw new RuntimeException("attempted to reduce Layout width");
556         }
557 
558         mWidth = wid;
559     }
560 
561     /**
562      * Return the total height of this layout.
563      */
getHeight()564     public int getHeight() {
565         return getLineTop(getLineCount());
566     }
567 
568     /**
569      * Return the base alignment of this layout.
570      */
getAlignment()571     public final Alignment getAlignment() {
572         return mAlignment;
573     }
574 
575     /**
576      * Return what the text height is multiplied by to get the line height.
577      */
getSpacingMultiplier()578     public final float getSpacingMultiplier() {
579         return mSpacingMult;
580     }
581 
582     /**
583      * Return the number of units of leading that are added to each line.
584      */
getSpacingAdd()585     public final float getSpacingAdd() {
586         return mSpacingAdd;
587     }
588 
589     /**
590      * Return the heuristic used to determine paragraph text direction.
591      * @hide
592      */
getTextDirectionHeuristic()593     public final TextDirectionHeuristic getTextDirectionHeuristic() {
594         return mTextDir;
595     }
596 
597     /**
598      * Return the number of lines of text in this layout.
599      */
600     public abstract int getLineCount();
601 
602     /**
603      * Return the baseline for the specified line (0&hellip;getLineCount() - 1)
604      * If bounds is not null, return the top, left, right, bottom extents
605      * of the specified line in it.
606      * @param line which line to examine (0..getLineCount() - 1)
607      * @param bounds Optional. If not null, it returns the extent of the line
608      * @return the Y-coordinate of the baseline
609      */
getLineBounds(int line, Rect bounds)610     public int getLineBounds(int line, Rect bounds) {
611         if (bounds != null) {
612             bounds.left = 0;     // ???
613             bounds.top = getLineTop(line);
614             bounds.right = mWidth;   // ???
615             bounds.bottom = getLineTop(line + 1);
616         }
617         return getLineBaseline(line);
618     }
619 
620     /**
621      * Return the vertical position of the top of the specified line
622      * (0&hellip;getLineCount()).
623      * If the specified line is equal to the line count, returns the
624      * bottom of the last line.
625      */
626     public abstract int getLineTop(int line);
627 
628     /**
629      * Return the descent of the specified line(0&hellip;getLineCount() - 1).
630      */
631     public abstract int getLineDescent(int line);
632 
633     /**
634      * Return the text offset of the beginning of the specified line (
635      * 0&hellip;getLineCount()). If the specified line is equal to the line
636      * count, returns the length of the text.
637      */
638     public abstract int getLineStart(int line);
639 
640     /**
641      * Returns the primary directionality of the paragraph containing the
642      * specified line, either 1 for left-to-right lines, or -1 for right-to-left
643      * lines (see {@link #DIR_LEFT_TO_RIGHT}, {@link #DIR_RIGHT_TO_LEFT}).
644      */
645     public abstract int getParagraphDirection(int line);
646 
647     /**
648      * Returns whether the specified line contains one or more
649      * characters that need to be handled specially, like tabs
650      * or emoji.
651      */
652     public abstract boolean getLineContainsTab(int line);
653 
654     /**
655      * Returns the directional run information for the specified line.
656      * The array alternates counts of characters in left-to-right
657      * and right-to-left segments of the line.
658      *
659      * <p>NOTE: this is inadequate to support bidirectional text, and will change.
660      */
661     public abstract Directions getLineDirections(int line);
662 
663     /**
664      * Returns the (negative) number of extra pixels of ascent padding in the
665      * top line of the Layout.
666      */
667     public abstract int getTopPadding();
668 
669     /**
670      * Returns the number of extra pixels of descent padding in the
671      * bottom line of the Layout.
672      */
673     public abstract int getBottomPadding();
674 
675 
676     /**
677      * Returns true if the character at offset and the preceding character
678      * are at different run levels (and thus there's a split caret).
679      * @param offset the offset
680      * @return true if at a level boundary
681      * @hide
682      */
isLevelBoundary(int offset)683     public boolean isLevelBoundary(int offset) {
684         int line = getLineForOffset(offset);
685         Directions dirs = getLineDirections(line);
686         if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) {
687             return false;
688         }
689 
690         int[] runs = dirs.mDirections;
691         int lineStart = getLineStart(line);
692         int lineEnd = getLineEnd(line);
693         if (offset == lineStart || offset == lineEnd) {
694             int paraLevel = getParagraphDirection(line) == 1 ? 0 : 1;
695             int runIndex = offset == lineStart ? 0 : runs.length - 2;
696             return ((runs[runIndex + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK) != paraLevel;
697         }
698 
699         offset -= lineStart;
700         for (int i = 0; i < runs.length; i += 2) {
701             if (offset == runs[i]) {
702                 return true;
703             }
704         }
705         return false;
706     }
707 
708     /**
709      * Returns true if the character at offset is right to left (RTL).
710      * @param offset the offset
711      * @return true if the character is RTL, false if it is LTR
712      */
isRtlCharAt(int offset)713     public boolean isRtlCharAt(int offset) {
714         int line = getLineForOffset(offset);
715         Directions dirs = getLineDirections(line);
716         if (dirs == DIRS_ALL_LEFT_TO_RIGHT) {
717             return false;
718         }
719         if (dirs == DIRS_ALL_RIGHT_TO_LEFT) {
720             return  true;
721         }
722         int[] runs = dirs.mDirections;
723         int lineStart = getLineStart(line);
724         for (int i = 0; i < runs.length; i += 2) {
725             int start = lineStart + (runs[i] & RUN_LENGTH_MASK);
726             // No need to test the end as an offset after the last run should return the value
727             // corresponding of the last run
728             if (offset >= start) {
729                 int level = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
730                 return ((level & 1) != 0);
731             }
732         }
733         // Should happen only if the offset is "out of bounds"
734         return false;
735     }
736 
primaryIsTrailingPrevious(int offset)737     private boolean primaryIsTrailingPrevious(int offset) {
738         int line = getLineForOffset(offset);
739         int lineStart = getLineStart(line);
740         int lineEnd = getLineEnd(line);
741         int[] runs = getLineDirections(line).mDirections;
742 
743         int levelAt = -1;
744         for (int i = 0; i < runs.length; i += 2) {
745             int start = lineStart + runs[i];
746             int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
747             if (limit > lineEnd) {
748                 limit = lineEnd;
749             }
750             if (offset >= start && offset < limit) {
751                 if (offset > start) {
752                     // Previous character is at same level, so don't use trailing.
753                     return false;
754                 }
755                 levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
756                 break;
757             }
758         }
759         if (levelAt == -1) {
760             // Offset was limit of line.
761             levelAt = getParagraphDirection(line) == 1 ? 0 : 1;
762         }
763 
764         // At level boundary, check previous level.
765         int levelBefore = -1;
766         if (offset == lineStart) {
767             levelBefore = getParagraphDirection(line) == 1 ? 0 : 1;
768         } else {
769             offset -= 1;
770             for (int i = 0; i < runs.length; i += 2) {
771                 int start = lineStart + runs[i];
772                 int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
773                 if (limit > lineEnd) {
774                     limit = lineEnd;
775                 }
776                 if (offset >= start && offset < limit) {
777                     levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
778                     break;
779                 }
780             }
781         }
782 
783         return levelBefore < levelAt;
784     }
785 
786     /**
787      * Get the primary horizontal position for the specified text offset.
788      * This is the location where a new character would be inserted in
789      * the paragraph's primary direction.
790      */
getPrimaryHorizontal(int offset)791     public float getPrimaryHorizontal(int offset) {
792         boolean trailing = primaryIsTrailingPrevious(offset);
793         return getHorizontal(offset, trailing);
794     }
795 
796     /**
797      * Get the secondary horizontal position for the specified text offset.
798      * This is the location where a new character would be inserted in
799      * the direction other than the paragraph's primary direction.
800      */
getSecondaryHorizontal(int offset)801     public float getSecondaryHorizontal(int offset) {
802         boolean trailing = primaryIsTrailingPrevious(offset);
803         return getHorizontal(offset, !trailing);
804     }
805 
getHorizontal(int offset, boolean trailing)806     private float getHorizontal(int offset, boolean trailing) {
807         int line = getLineForOffset(offset);
808 
809         return getHorizontal(offset, trailing, line);
810     }
811 
getHorizontal(int offset, boolean trailing, int line)812     private float getHorizontal(int offset, boolean trailing, int line) {
813         int start = getLineStart(line);
814         int end = getLineEnd(line);
815         int dir = getParagraphDirection(line);
816         boolean hasTabOrEmoji = getLineContainsTab(line);
817         Directions directions = getLineDirections(line);
818 
819         TabStops tabStops = null;
820         if (hasTabOrEmoji && mText instanceof Spanned) {
821             // Just checking this line should be good enough, tabs should be
822             // consistent across all lines in a paragraph.
823             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
824             if (tabs.length > 0) {
825                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
826             }
827         }
828 
829         TextLine tl = TextLine.obtain();
830         tl.set(mPaint, mText, start, end, dir, directions, hasTabOrEmoji, tabStops);
831         float wid = tl.measure(offset - start, trailing, null);
832         TextLine.recycle(tl);
833 
834         int left = getParagraphLeft(line);
835         int right = getParagraphRight(line);
836 
837         return getLineStartPos(line, left, right) + wid;
838     }
839 
840     /**
841      * Get the leftmost position that should be exposed for horizontal
842      * scrolling on the specified line.
843      */
getLineLeft(int line)844     public float getLineLeft(int line) {
845         int dir = getParagraphDirection(line);
846         Alignment align = getParagraphAlignment(line);
847 
848         if (align == Alignment.ALIGN_LEFT) {
849             return 0;
850         } else if (align == Alignment.ALIGN_NORMAL) {
851             if (dir == DIR_RIGHT_TO_LEFT)
852                 return getParagraphRight(line) - getLineMax(line);
853             else
854                 return 0;
855         } else if (align == Alignment.ALIGN_RIGHT) {
856             return mWidth - getLineMax(line);
857         } else if (align == Alignment.ALIGN_OPPOSITE) {
858             if (dir == DIR_RIGHT_TO_LEFT)
859                 return 0;
860             else
861                 return mWidth - getLineMax(line);
862         } else { /* align == Alignment.ALIGN_CENTER */
863             int left = getParagraphLeft(line);
864             int right = getParagraphRight(line);
865             int max = ((int) getLineMax(line)) & ~1;
866 
867             return left + ((right - left) - max) / 2;
868         }
869     }
870 
871     /**
872      * Get the rightmost position that should be exposed for horizontal
873      * scrolling on the specified line.
874      */
getLineRight(int line)875     public float getLineRight(int line) {
876         int dir = getParagraphDirection(line);
877         Alignment align = getParagraphAlignment(line);
878 
879         if (align == Alignment.ALIGN_LEFT) {
880             return getParagraphLeft(line) + getLineMax(line);
881         } else if (align == Alignment.ALIGN_NORMAL) {
882             if (dir == DIR_RIGHT_TO_LEFT)
883                 return mWidth;
884             else
885                 return getParagraphLeft(line) + getLineMax(line);
886         } else if (align == Alignment.ALIGN_RIGHT) {
887             return mWidth;
888         } else if (align == Alignment.ALIGN_OPPOSITE) {
889             if (dir == DIR_RIGHT_TO_LEFT)
890                 return getLineMax(line);
891             else
892                 return mWidth;
893         } else { /* align == Alignment.ALIGN_CENTER */
894             int left = getParagraphLeft(line);
895             int right = getParagraphRight(line);
896             int max = ((int) getLineMax(line)) & ~1;
897 
898             return right - ((right - left) - max) / 2;
899         }
900     }
901 
902     /**
903      * Gets the unsigned horizontal extent of the specified line, including
904      * leading margin indent, but excluding trailing whitespace.
905      */
getLineMax(int line)906     public float getLineMax(int line) {
907         float margin = getParagraphLeadingMargin(line);
908         float signedExtent = getLineExtent(line, false);
909         return margin + signedExtent >= 0 ? signedExtent : -signedExtent;
910     }
911 
912     /**
913      * Gets the unsigned horizontal extent of the specified line, including
914      * leading margin indent and trailing whitespace.
915      */
getLineWidth(int line)916     public float getLineWidth(int line) {
917         float margin = getParagraphLeadingMargin(line);
918         float signedExtent = getLineExtent(line, true);
919         return margin + signedExtent >= 0 ? signedExtent : -signedExtent;
920     }
921 
922     /**
923      * Like {@link #getLineExtent(int,TabStops,boolean)} but determines the
924      * tab stops instead of using the ones passed in.
925      * @param line the index of the line
926      * @param full whether to include trailing whitespace
927      * @return the extent of the line
928      */
getLineExtent(int line, boolean full)929     private float getLineExtent(int line, boolean full) {
930         int start = getLineStart(line);
931         int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
932 
933         boolean hasTabsOrEmoji = getLineContainsTab(line);
934         TabStops tabStops = null;
935         if (hasTabsOrEmoji && mText instanceof Spanned) {
936             // Just checking this line should be good enough, tabs should be
937             // consistent across all lines in a paragraph.
938             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
939             if (tabs.length > 0) {
940                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
941             }
942         }
943         Directions directions = getLineDirections(line);
944         // Returned directions can actually be null
945         if (directions == null) {
946             return 0f;
947         }
948         int dir = getParagraphDirection(line);
949 
950         TextLine tl = TextLine.obtain();
951         tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops);
952         float width = tl.metrics(null);
953         TextLine.recycle(tl);
954         return width;
955     }
956 
957     /**
958      * Returns the signed horizontal extent of the specified line, excluding
959      * leading margin.  If full is false, excludes trailing whitespace.
960      * @param line the index of the line
961      * @param tabStops the tab stops, can be null if we know they're not used.
962      * @param full whether to include trailing whitespace
963      * @return the extent of the text on this line
964      */
getLineExtent(int line, TabStops tabStops, boolean full)965     private float getLineExtent(int line, TabStops tabStops, boolean full) {
966         int start = getLineStart(line);
967         int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
968         boolean hasTabsOrEmoji = getLineContainsTab(line);
969         Directions directions = getLineDirections(line);
970         int dir = getParagraphDirection(line);
971 
972         TextLine tl = TextLine.obtain();
973         tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops);
974         float width = tl.metrics(null);
975         TextLine.recycle(tl);
976         return width;
977     }
978 
979     /**
980      * Get the line number corresponding to the specified vertical position.
981      * If you ask for a position above 0, you get 0; if you ask for a position
982      * below the bottom of the text, you get the last line.
983      */
984     // FIXME: It may be faster to do a linear search for layouts without many lines.
getLineForVertical(int vertical)985     public int getLineForVertical(int vertical) {
986         int high = getLineCount(), low = -1, guess;
987 
988         while (high - low > 1) {
989             guess = (high + low) / 2;
990 
991             if (getLineTop(guess) > vertical)
992                 high = guess;
993             else
994                 low = guess;
995         }
996 
997         if (low < 0)
998             return 0;
999         else
1000             return low;
1001     }
1002 
1003     /**
1004      * Get the line number on which the specified text offset appears.
1005      * If you ask for a position before 0, you get 0; if you ask for a position
1006      * beyond the end of the text, you get the last line.
1007      */
getLineForOffset(int offset)1008     public int getLineForOffset(int offset) {
1009         int high = getLineCount(), low = -1, guess;
1010 
1011         while (high - low > 1) {
1012             guess = (high + low) / 2;
1013 
1014             if (getLineStart(guess) > offset)
1015                 high = guess;
1016             else
1017                 low = guess;
1018         }
1019 
1020         if (low < 0)
1021             return 0;
1022         else
1023             return low;
1024     }
1025 
1026     /**
1027      * Get the character offset on the specified line whose position is
1028      * closest to the specified horizontal position.
1029      */
getOffsetForHorizontal(int line, float horiz)1030     public int getOffsetForHorizontal(int line, float horiz) {
1031         int max = getLineEnd(line) - 1;
1032         int min = getLineStart(line);
1033         Directions dirs = getLineDirections(line);
1034 
1035         if (line == getLineCount() - 1)
1036             max++;
1037 
1038         int best = min;
1039         float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz);
1040 
1041         for (int i = 0; i < dirs.mDirections.length; i += 2) {
1042             int here = min + dirs.mDirections[i];
1043             int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK);
1044             int swap = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0 ? -1 : 1;
1045 
1046             if (there > max)
1047                 there = max;
1048             int high = there - 1 + 1, low = here + 1 - 1, guess;
1049 
1050             while (high - low > 1) {
1051                 guess = (high + low) / 2;
1052                 int adguess = getOffsetAtStartOf(guess);
1053 
1054                 if (getPrimaryHorizontal(adguess) * swap >= horiz * swap)
1055                     high = guess;
1056                 else
1057                     low = guess;
1058             }
1059 
1060             if (low < here + 1)
1061                 low = here + 1;
1062 
1063             if (low < there) {
1064                 low = getOffsetAtStartOf(low);
1065 
1066                 float dist = Math.abs(getPrimaryHorizontal(low) - horiz);
1067 
1068                 int aft = TextUtils.getOffsetAfter(mText, low);
1069                 if (aft < there) {
1070                     float other = Math.abs(getPrimaryHorizontal(aft) - horiz);
1071 
1072                     if (other < dist) {
1073                         dist = other;
1074                         low = aft;
1075                     }
1076                 }
1077 
1078                 if (dist < bestdist) {
1079                     bestdist = dist;
1080                     best = low;
1081                 }
1082             }
1083 
1084             float dist = Math.abs(getPrimaryHorizontal(here) - horiz);
1085 
1086             if (dist < bestdist) {
1087                 bestdist = dist;
1088                 best = here;
1089             }
1090         }
1091 
1092         float dist = Math.abs(getPrimaryHorizontal(max) - horiz);
1093 
1094         if (dist < bestdist) {
1095             bestdist = dist;
1096             best = max;
1097         }
1098 
1099         return best;
1100     }
1101 
1102     /**
1103      * Return the text offset after the last character on the specified line.
1104      */
getLineEnd(int line)1105     public final int getLineEnd(int line) {
1106         return getLineStart(line + 1);
1107     }
1108 
1109     /**
1110      * Return the text offset after the last visible character (so whitespace
1111      * is not counted) on the specified line.
1112      */
getLineVisibleEnd(int line)1113     public int getLineVisibleEnd(int line) {
1114         return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1));
1115     }
1116 
getLineVisibleEnd(int line, int start, int end)1117     private int getLineVisibleEnd(int line, int start, int end) {
1118         CharSequence text = mText;
1119         char ch;
1120         if (line == getLineCount() - 1) {
1121             return end;
1122         }
1123 
1124         for (; end > start; end--) {
1125             ch = text.charAt(end - 1);
1126 
1127             if (ch == '\n') {
1128                 return end - 1;
1129             }
1130 
1131             if (ch != ' ' && ch != '\t') {
1132                 break;
1133             }
1134 
1135         }
1136 
1137         return end;
1138     }
1139 
1140     /**
1141      * Return the vertical position of the bottom of the specified line.
1142      */
getLineBottom(int line)1143     public final int getLineBottom(int line) {
1144         return getLineTop(line + 1);
1145     }
1146 
1147     /**
1148      * Return the vertical position of the baseline of the specified line.
1149      */
getLineBaseline(int line)1150     public final int getLineBaseline(int line) {
1151         // getLineTop(line+1) == getLineTop(line)
1152         return getLineTop(line+1) - getLineDescent(line);
1153     }
1154 
1155     /**
1156      * Get the ascent of the text on the specified line.
1157      * The return value is negative to match the Paint.ascent() convention.
1158      */
getLineAscent(int line)1159     public final int getLineAscent(int line) {
1160         // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line)
1161         return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line));
1162     }
1163 
getOffsetToLeftOf(int offset)1164     public int getOffsetToLeftOf(int offset) {
1165         return getOffsetToLeftRightOf(offset, true);
1166     }
1167 
getOffsetToRightOf(int offset)1168     public int getOffsetToRightOf(int offset) {
1169         return getOffsetToLeftRightOf(offset, false);
1170     }
1171 
getOffsetToLeftRightOf(int caret, boolean toLeft)1172     private int getOffsetToLeftRightOf(int caret, boolean toLeft) {
1173         int line = getLineForOffset(caret);
1174         int lineStart = getLineStart(line);
1175         int lineEnd = getLineEnd(line);
1176         int lineDir = getParagraphDirection(line);
1177 
1178         boolean lineChanged = false;
1179         boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT);
1180         // if walking off line, look at the line we're headed to
1181         if (advance) {
1182             if (caret == lineEnd) {
1183                 if (line < getLineCount() - 1) {
1184                     lineChanged = true;
1185                     ++line;
1186                 } else {
1187                     return caret; // at very end, don't move
1188                 }
1189             }
1190         } else {
1191             if (caret == lineStart) {
1192                 if (line > 0) {
1193                     lineChanged = true;
1194                     --line;
1195                 } else {
1196                     return caret; // at very start, don't move
1197                 }
1198             }
1199         }
1200 
1201         if (lineChanged) {
1202             lineStart = getLineStart(line);
1203             lineEnd = getLineEnd(line);
1204             int newDir = getParagraphDirection(line);
1205             if (newDir != lineDir) {
1206                 // unusual case.  we want to walk onto the line, but it runs
1207                 // in a different direction than this one, so we fake movement
1208                 // in the opposite direction.
1209                 toLeft = !toLeft;
1210                 lineDir = newDir;
1211             }
1212         }
1213 
1214         Directions directions = getLineDirections(line);
1215 
1216         TextLine tl = TextLine.obtain();
1217         // XXX: we don't care about tabs
1218         tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null);
1219         caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft);
1220         tl = TextLine.recycle(tl);
1221         return caret;
1222     }
1223 
getOffsetAtStartOf(int offset)1224     private int getOffsetAtStartOf(int offset) {
1225         // XXX this probably should skip local reorderings and
1226         // zero-width characters, look at callers
1227         if (offset == 0)
1228             return 0;
1229 
1230         CharSequence text = mText;
1231         char c = text.charAt(offset);
1232 
1233         if (c >= '\uDC00' && c <= '\uDFFF') {
1234             char c1 = text.charAt(offset - 1);
1235 
1236             if (c1 >= '\uD800' && c1 <= '\uDBFF')
1237                 offset -= 1;
1238         }
1239 
1240         if (mSpannedText) {
1241             ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
1242                                                        ReplacementSpan.class);
1243 
1244             for (int i = 0; i < spans.length; i++) {
1245                 int start = ((Spanned) text).getSpanStart(spans[i]);
1246                 int end = ((Spanned) text).getSpanEnd(spans[i]);
1247 
1248                 if (start < offset && end > offset)
1249                     offset = start;
1250             }
1251         }
1252 
1253         return offset;
1254     }
1255 
1256     /**
1257      * Fills in the specified Path with a representation of a cursor
1258      * at the specified offset.  This will often be a vertical line
1259      * but can be multiple discontinuous lines in text with multiple
1260      * directionalities.
1261      */
getCursorPath(int point, Path dest, CharSequence editingBuffer)1262     public void getCursorPath(int point, Path dest,
1263                               CharSequence editingBuffer) {
1264         dest.reset();
1265 
1266         int line = getLineForOffset(point);
1267         int top = getLineTop(line);
1268         int bottom = getLineTop(line+1);
1269 
1270         float h1 = getPrimaryHorizontal(point) - 0.5f;
1271         float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point) - 0.5f : h1;
1272 
1273         int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) |
1274                    TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING);
1275         int fn = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_ALT_ON);
1276         int dist = 0;
1277 
1278         if (caps != 0 || fn != 0) {
1279             dist = (bottom - top) >> 2;
1280 
1281             if (fn != 0)
1282                 top += dist;
1283             if (caps != 0)
1284                 bottom -= dist;
1285         }
1286 
1287         if (h1 < 0.5f)
1288             h1 = 0.5f;
1289         if (h2 < 0.5f)
1290             h2 = 0.5f;
1291 
1292         if (Float.compare(h1, h2) == 0) {
1293             dest.moveTo(h1, top);
1294             dest.lineTo(h1, bottom);
1295         } else {
1296             dest.moveTo(h1, top);
1297             dest.lineTo(h1, (top + bottom) >> 1);
1298 
1299             dest.moveTo(h2, (top + bottom) >> 1);
1300             dest.lineTo(h2, bottom);
1301         }
1302 
1303         if (caps == 2) {
1304             dest.moveTo(h2, bottom);
1305             dest.lineTo(h2 - dist, bottom + dist);
1306             dest.lineTo(h2, bottom);
1307             dest.lineTo(h2 + dist, bottom + dist);
1308         } else if (caps == 1) {
1309             dest.moveTo(h2, bottom);
1310             dest.lineTo(h2 - dist, bottom + dist);
1311 
1312             dest.moveTo(h2 - dist, bottom + dist - 0.5f);
1313             dest.lineTo(h2 + dist, bottom + dist - 0.5f);
1314 
1315             dest.moveTo(h2 + dist, bottom + dist);
1316             dest.lineTo(h2, bottom);
1317         }
1318 
1319         if (fn == 2) {
1320             dest.moveTo(h1, top);
1321             dest.lineTo(h1 - dist, top - dist);
1322             dest.lineTo(h1, top);
1323             dest.lineTo(h1 + dist, top - dist);
1324         } else if (fn == 1) {
1325             dest.moveTo(h1, top);
1326             dest.lineTo(h1 - dist, top - dist);
1327 
1328             dest.moveTo(h1 - dist, top - dist + 0.5f);
1329             dest.lineTo(h1 + dist, top - dist + 0.5f);
1330 
1331             dest.moveTo(h1 + dist, top - dist);
1332             dest.lineTo(h1, top);
1333         }
1334     }
1335 
addSelection(int line, int start, int end, int top, int bottom, Path dest)1336     private void addSelection(int line, int start, int end,
1337                               int top, int bottom, Path dest) {
1338         int linestart = getLineStart(line);
1339         int lineend = getLineEnd(line);
1340         Directions dirs = getLineDirections(line);
1341 
1342         if (lineend > linestart && mText.charAt(lineend - 1) == '\n')
1343             lineend--;
1344 
1345         for (int i = 0; i < dirs.mDirections.length; i += 2) {
1346             int here = linestart + dirs.mDirections[i];
1347             int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK);
1348 
1349             if (there > lineend)
1350                 there = lineend;
1351 
1352             if (start <= there && end >= here) {
1353                 int st = Math.max(start, here);
1354                 int en = Math.min(end, there);
1355 
1356                 if (st != en) {
1357                     float h1 = getHorizontal(st, false, line);
1358                     float h2 = getHorizontal(en, true, line);
1359 
1360                     float left = Math.min(h1, h2);
1361                     float right = Math.max(h1, h2);
1362 
1363                     dest.addRect(left, top, right, bottom, Path.Direction.CW);
1364                 }
1365             }
1366         }
1367     }
1368 
1369     /**
1370      * Fills in the specified Path with a representation of a highlight
1371      * between the specified offsets.  This will often be a rectangle
1372      * or a potentially discontinuous set of rectangles.  If the start
1373      * and end are the same, the returned path is empty.
1374      */
getSelectionPath(int start, int end, Path dest)1375     public void getSelectionPath(int start, int end, Path dest) {
1376         dest.reset();
1377 
1378         if (start == end)
1379             return;
1380 
1381         if (end < start) {
1382             int temp = end;
1383             end = start;
1384             start = temp;
1385         }
1386 
1387         int startline = getLineForOffset(start);
1388         int endline = getLineForOffset(end);
1389 
1390         int top = getLineTop(startline);
1391         int bottom = getLineBottom(endline);
1392 
1393         if (startline == endline) {
1394             addSelection(startline, start, end, top, bottom, dest);
1395         } else {
1396             final float width = mWidth;
1397 
1398             addSelection(startline, start, getLineEnd(startline),
1399                          top, getLineBottom(startline), dest);
1400 
1401             if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT)
1402                 dest.addRect(getLineLeft(startline), top,
1403                               0, getLineBottom(startline), Path.Direction.CW);
1404             else
1405                 dest.addRect(getLineRight(startline), top,
1406                               width, getLineBottom(startline), Path.Direction.CW);
1407 
1408             for (int i = startline + 1; i < endline; i++) {
1409                 top = getLineTop(i);
1410                 bottom = getLineBottom(i);
1411                 dest.addRect(0, top, width, bottom, Path.Direction.CW);
1412             }
1413 
1414             top = getLineTop(endline);
1415             bottom = getLineBottom(endline);
1416 
1417             addSelection(endline, getLineStart(endline), end,
1418                          top, bottom, dest);
1419 
1420             if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT)
1421                 dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW);
1422             else
1423                 dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW);
1424         }
1425     }
1426 
1427     /**
1428      * Get the alignment of the specified paragraph, taking into account
1429      * markup attached to it.
1430      */
getParagraphAlignment(int line)1431     public final Alignment getParagraphAlignment(int line) {
1432         Alignment align = mAlignment;
1433 
1434         if (mSpannedText) {
1435             Spanned sp = (Spanned) mText;
1436             AlignmentSpan[] spans = getParagraphSpans(sp, getLineStart(line),
1437                                                 getLineEnd(line),
1438                                                 AlignmentSpan.class);
1439 
1440             int spanLength = spans.length;
1441             if (spanLength > 0) {
1442                 align = spans[spanLength-1].getAlignment();
1443             }
1444         }
1445 
1446         return align;
1447     }
1448 
1449     /**
1450      * Get the left edge of the specified paragraph, inset by left margins.
1451      */
getParagraphLeft(int line)1452     public final int getParagraphLeft(int line) {
1453         int left = 0;
1454         int dir = getParagraphDirection(line);
1455         if (dir == DIR_RIGHT_TO_LEFT || !mSpannedText) {
1456             return left; // leading margin has no impact, or no styles
1457         }
1458         return getParagraphLeadingMargin(line);
1459     }
1460 
1461     /**
1462      * Get the right edge of the specified paragraph, inset by right margins.
1463      */
getParagraphRight(int line)1464     public final int getParagraphRight(int line) {
1465         int right = mWidth;
1466         int dir = getParagraphDirection(line);
1467         if (dir == DIR_LEFT_TO_RIGHT || !mSpannedText) {
1468             return right; // leading margin has no impact, or no styles
1469         }
1470         return right - getParagraphLeadingMargin(line);
1471     }
1472 
1473     /**
1474      * Returns the effective leading margin (unsigned) for this line,
1475      * taking into account LeadingMarginSpan and LeadingMarginSpan2.
1476      * @param line the line index
1477      * @return the leading margin of this line
1478      */
getParagraphLeadingMargin(int line)1479     private int getParagraphLeadingMargin(int line) {
1480         if (!mSpannedText) {
1481             return 0;
1482         }
1483         Spanned spanned = (Spanned) mText;
1484 
1485         int lineStart = getLineStart(line);
1486         int lineEnd = getLineEnd(line);
1487         int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd,
1488                 LeadingMarginSpan.class);
1489         LeadingMarginSpan[] spans = getParagraphSpans(spanned, lineStart, spanEnd,
1490                                                 LeadingMarginSpan.class);
1491         if (spans.length == 0) {
1492             return 0; // no leading margin span;
1493         }
1494 
1495         int margin = 0;
1496 
1497         boolean isFirstParaLine = lineStart == 0 ||
1498             spanned.charAt(lineStart - 1) == '\n';
1499 
1500         for (int i = 0; i < spans.length; i++) {
1501             LeadingMarginSpan span = spans[i];
1502             boolean useFirstLineMargin = isFirstParaLine;
1503             if (span instanceof LeadingMarginSpan2) {
1504                 int spStart = spanned.getSpanStart(span);
1505                 int spanLine = getLineForOffset(spStart);
1506                 int count = ((LeadingMarginSpan2)span).getLeadingMarginLineCount();
1507                 useFirstLineMargin = line < spanLine + count;
1508             }
1509             margin += span.getLeadingMargin(useFirstLineMargin);
1510         }
1511 
1512         return margin;
1513     }
1514 
1515     /* package */
1516     static float measurePara(TextPaint paint, CharSequence text, int start, int end) {
1517 
1518         MeasuredText mt = MeasuredText.obtain();
1519         TextLine tl = TextLine.obtain();
1520         try {
1521             mt.setPara(text, start, end, TextDirectionHeuristics.LTR);
1522             Directions directions;
1523             int dir;
1524             if (mt.mEasy) {
1525                 directions = DIRS_ALL_LEFT_TO_RIGHT;
1526                 dir = Layout.DIR_LEFT_TO_RIGHT;
1527             } else {
1528                 directions = AndroidBidi.directions(mt.mDir, mt.mLevels,
1529                     0, mt.mChars, 0, mt.mLen);
1530                 dir = mt.mDir;
1531             }
1532             char[] chars = mt.mChars;
1533             int len = mt.mLen;
1534             boolean hasTabs = false;
1535             TabStops tabStops = null;
1536             for (int i = 0; i < len; ++i) {
1537                 if (chars[i] == '\t') {
1538                     hasTabs = true;
1539                     if (text instanceof Spanned) {
1540                         Spanned spanned = (Spanned) text;
1541                         int spanEnd = spanned.nextSpanTransition(start, end,
1542                                 TabStopSpan.class);
1543                         TabStopSpan[] spans = getParagraphSpans(spanned, start, spanEnd,
1544                                 TabStopSpan.class);
1545                         if (spans.length > 0) {
1546                             tabStops = new TabStops(TAB_INCREMENT, spans);
1547                         }
1548                     }
1549                     break;
1550                 }
1551             }
1552             tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops);
1553             return tl.metrics(null);
1554         } finally {
1555             TextLine.recycle(tl);
1556             MeasuredText.recycle(mt);
1557         }
1558     }
1559 
1560     /**
1561      * @hide
1562      */
1563     /* package */ static class TabStops {
1564         private int[] mStops;
1565         private int mNumStops;
1566         private int mIncrement;
1567 
1568         TabStops(int increment, Object[] spans) {
1569             reset(increment, spans);
1570         }
1571 
1572         void reset(int increment, Object[] spans) {
1573             this.mIncrement = increment;
1574 
1575             int ns = 0;
1576             if (spans != null) {
1577                 int[] stops = this.mStops;
1578                 for (Object o : spans) {
1579                     if (o instanceof TabStopSpan) {
1580                         if (stops == null) {
1581                             stops = new int[10];
1582                         } else if (ns == stops.length) {
1583                             int[] nstops = new int[ns * 2];
1584                             for (int i = 0; i < ns; ++i) {
1585                                 nstops[i] = stops[i];
1586                             }
1587                             stops = nstops;
1588                         }
1589                         stops[ns++] = ((TabStopSpan) o).getTabStop();
1590                     }
1591                 }
1592                 if (ns > 1) {
1593                     Arrays.sort(stops, 0, ns);
1594                 }
1595                 if (stops != this.mStops) {
1596                     this.mStops = stops;
1597                 }
1598             }
1599             this.mNumStops = ns;
1600         }
1601 
1602         float nextTab(float h) {
1603             int ns = this.mNumStops;
1604             if (ns > 0) {
1605                 int[] stops = this.mStops;
1606                 for (int i = 0; i < ns; ++i) {
1607                     int stop = stops[i];
1608                     if (stop > h) {
1609                         return stop;
1610                     }
1611                 }
1612             }
1613             return nextDefaultStop(h, mIncrement);
1614         }
1615 
nextDefaultStop(float h, int inc)1616         public static float nextDefaultStop(float h, int inc) {
1617             return ((int) ((h + inc) / inc)) * inc;
1618         }
1619     }
1620 
1621     /**
1622      * Returns the position of the next tab stop after h on the line.
1623      *
1624      * @param text the text
1625      * @param start start of the line
1626      * @param end limit of the line
1627      * @param h the current horizontal offset
1628      * @param tabs the tabs, can be null.  If it is null, any tabs in effect
1629      * on the line will be used.  If there are no tabs, a default offset
1630      * will be used to compute the tab stop.
1631      * @return the offset of the next tab stop.
1632      */
nextTab(CharSequence text, int start, int end, float h, Object[] tabs)1633     /* package */ static float nextTab(CharSequence text, int start, int end,
1634                                        float h, Object[] tabs) {
1635         float nh = Float.MAX_VALUE;
1636         boolean alltabs = false;
1637 
1638         if (text instanceof Spanned) {
1639             if (tabs == null) {
1640                 tabs = getParagraphSpans((Spanned) text, start, end, TabStopSpan.class);
1641                 alltabs = true;
1642             }
1643 
1644             for (int i = 0; i < tabs.length; i++) {
1645                 if (!alltabs) {
1646                     if (!(tabs[i] instanceof TabStopSpan))
1647                         continue;
1648                 }
1649 
1650                 int where = ((TabStopSpan) tabs[i]).getTabStop();
1651 
1652                 if (where < nh && where > h)
1653                     nh = where;
1654             }
1655 
1656             if (nh != Float.MAX_VALUE)
1657                 return nh;
1658         }
1659 
1660         return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT;
1661     }
1662 
isSpanned()1663     protected final boolean isSpanned() {
1664         return mSpannedText;
1665     }
1666 
1667     /**
1668      * Returns the same as <code>text.getSpans()</code>, except where
1669      * <code>start</code> and <code>end</code> are the same and are not
1670      * at the very beginning of the text, in which case an empty array
1671      * is returned instead.
1672      * <p>
1673      * This is needed because of the special case that <code>getSpans()</code>
1674      * on an empty range returns the spans adjacent to that range, which is
1675      * primarily for the sake of <code>TextWatchers</code> so they will get
1676      * notifications when text goes from empty to non-empty.  But it also
1677      * has the unfortunate side effect that if the text ends with an empty
1678      * paragraph, that paragraph accidentally picks up the styles of the
1679      * preceding paragraph (even though those styles will not be picked up
1680      * by new text that is inserted into the empty paragraph).
1681      * <p>
1682      * The reason it just checks whether <code>start</code> and <code>end</code>
1683      * is the same is that the only time a line can contain 0 characters
1684      * is if it is the final paragraph of the Layout; otherwise any line will
1685      * contain at least one printing or newline character.  The reason for the
1686      * additional check if <code>start</code> is greater than 0 is that
1687      * if the empty paragraph is the entire content of the buffer, paragraph
1688      * styles that are already applied to the buffer will apply to text that
1689      * is inserted into it.
1690      */
getParagraphSpans(Spanned text, int start, int end, Class<T> type)1691     /* package */static <T> T[] getParagraphSpans(Spanned text, int start, int end, Class<T> type) {
1692         if (start == end && start > 0) {
1693             return ArrayUtils.emptyArray(type);
1694         }
1695 
1696         return text.getSpans(start, end, type);
1697     }
1698 
getEllipsisChar(TextUtils.TruncateAt method)1699     private char getEllipsisChar(TextUtils.TruncateAt method) {
1700         return (method == TextUtils.TruncateAt.END_SMALL) ?
1701                 ELLIPSIS_TWO_DOTS[0] :
1702                 ELLIPSIS_NORMAL[0];
1703     }
1704 
ellipsize(int start, int end, int line, char[] dest, int destoff, TextUtils.TruncateAt method)1705     private void ellipsize(int start, int end, int line,
1706                            char[] dest, int destoff, TextUtils.TruncateAt method) {
1707         int ellipsisCount = getEllipsisCount(line);
1708 
1709         if (ellipsisCount == 0) {
1710             return;
1711         }
1712 
1713         int ellipsisStart = getEllipsisStart(line);
1714         int linestart = getLineStart(line);
1715 
1716         for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) {
1717             char c;
1718 
1719             if (i == ellipsisStart) {
1720                 c = getEllipsisChar(method); // ellipsis
1721             } else {
1722                 c = '\uFEFF'; // 0-width space
1723             }
1724 
1725             int a = i + linestart;
1726 
1727             if (a >= start && a < end) {
1728                 dest[destoff + a - start] = c;
1729             }
1730         }
1731     }
1732 
1733     /**
1734      * Stores information about bidirectional (left-to-right or right-to-left)
1735      * text within the layout of a line.
1736      */
1737     public static class Directions {
1738         // Directions represents directional runs within a line of text.
1739         // Runs are pairs of ints listed in visual order, starting from the
1740         // leading margin.  The first int of each pair is the offset from
1741         // the first character of the line to the start of the run.  The
1742         // second int represents both the length and level of the run.
1743         // The length is in the lower bits, accessed by masking with
1744         // DIR_LENGTH_MASK.  The level is in the higher bits, accessed
1745         // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK.
1746         // To simply test for an RTL direction, test the bit using
1747         // DIR_RTL_FLAG, if set then the direction is rtl.
1748 
1749         /* package */ int[] mDirections;
Directions(int[] dirs)1750         /* package */ Directions(int[] dirs) {
1751             mDirections = dirs;
1752         }
1753     }
1754 
1755     /**
1756      * Return the offset of the first character to be ellipsized away,
1757      * relative to the start of the line.  (So 0 if the beginning of the
1758      * line is ellipsized, not getLineStart().)
1759      */
1760     public abstract int getEllipsisStart(int line);
1761 
1762     /**
1763      * Returns the number of characters to be ellipsized away, or 0 if
1764      * no ellipsis is to take place.
1765      */
1766     public abstract int getEllipsisCount(int line);
1767 
1768     /* package */ static class Ellipsizer implements CharSequence, GetChars {
1769         /* package */ CharSequence mText;
1770         /* package */ Layout mLayout;
1771         /* package */ int mWidth;
1772         /* package */ TextUtils.TruncateAt mMethod;
1773 
Ellipsizer(CharSequence s)1774         public Ellipsizer(CharSequence s) {
1775             mText = s;
1776         }
1777 
charAt(int off)1778         public char charAt(int off) {
1779             char[] buf = TextUtils.obtain(1);
1780             getChars(off, off + 1, buf, 0);
1781             char ret = buf[0];
1782 
1783             TextUtils.recycle(buf);
1784             return ret;
1785         }
1786 
getChars(int start, int end, char[] dest, int destoff)1787         public void getChars(int start, int end, char[] dest, int destoff) {
1788             int line1 = mLayout.getLineForOffset(start);
1789             int line2 = mLayout.getLineForOffset(end);
1790 
1791             TextUtils.getChars(mText, start, end, dest, destoff);
1792 
1793             for (int i = line1; i <= line2; i++) {
1794                 mLayout.ellipsize(start, end, i, dest, destoff, mMethod);
1795             }
1796         }
1797 
length()1798         public int length() {
1799             return mText.length();
1800         }
1801 
subSequence(int start, int end)1802         public CharSequence subSequence(int start, int end) {
1803             char[] s = new char[end - start];
1804             getChars(start, end, s, 0);
1805             return new String(s);
1806         }
1807 
1808         @Override
toString()1809         public String toString() {
1810             char[] s = new char[length()];
1811             getChars(0, length(), s, 0);
1812             return new String(s);
1813         }
1814 
1815     }
1816 
1817     /* package */ static class SpannedEllipsizer extends Ellipsizer implements Spanned {
1818         private Spanned mSpanned;
1819 
SpannedEllipsizer(CharSequence display)1820         public SpannedEllipsizer(CharSequence display) {
1821             super(display);
1822             mSpanned = (Spanned) display;
1823         }
1824 
getSpans(int start, int end, Class<T> type)1825         public <T> T[] getSpans(int start, int end, Class<T> type) {
1826             return mSpanned.getSpans(start, end, type);
1827         }
1828 
getSpanStart(Object tag)1829         public int getSpanStart(Object tag) {
1830             return mSpanned.getSpanStart(tag);
1831         }
1832 
getSpanEnd(Object tag)1833         public int getSpanEnd(Object tag) {
1834             return mSpanned.getSpanEnd(tag);
1835         }
1836 
getSpanFlags(Object tag)1837         public int getSpanFlags(Object tag) {
1838             return mSpanned.getSpanFlags(tag);
1839         }
1840 
1841         @SuppressWarnings("rawtypes")
nextSpanTransition(int start, int limit, Class type)1842         public int nextSpanTransition(int start, int limit, Class type) {
1843             return mSpanned.nextSpanTransition(start, limit, type);
1844         }
1845 
1846         @Override
subSequence(int start, int end)1847         public CharSequence subSequence(int start, int end) {
1848             char[] s = new char[end - start];
1849             getChars(start, end, s, 0);
1850 
1851             SpannableString ss = new SpannableString(new String(s));
1852             TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0);
1853             return ss;
1854         }
1855     }
1856 
1857     private CharSequence mText;
1858     private TextPaint mPaint;
1859     /* package */ TextPaint mWorkPaint;
1860     private int mWidth;
1861     private Alignment mAlignment = Alignment.ALIGN_NORMAL;
1862     private float mSpacingMult;
1863     private float mSpacingAdd;
1864     private static final Rect sTempRect = new Rect();
1865     private boolean mSpannedText;
1866     private TextDirectionHeuristic mTextDir;
1867     private SpanSet<LineBackgroundSpan> mLineBackgroundSpans;
1868 
1869     public static final int DIR_LEFT_TO_RIGHT = 1;
1870     public static final int DIR_RIGHT_TO_LEFT = -1;
1871 
1872     /* package */ static final int DIR_REQUEST_LTR = 1;
1873     /* package */ static final int DIR_REQUEST_RTL = -1;
1874     /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2;
1875     /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2;
1876 
1877     /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff;
1878     /* package */ static final int RUN_LEVEL_SHIFT = 26;
1879     /* package */ static final int RUN_LEVEL_MASK = 0x3f;
1880     /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT;
1881 
1882     public enum Alignment {
1883         ALIGN_NORMAL,
1884         ALIGN_OPPOSITE,
1885         ALIGN_CENTER,
1886         /** @hide */
1887         ALIGN_LEFT,
1888         /** @hide */
1889         ALIGN_RIGHT,
1890     }
1891 
1892     private static final int TAB_INCREMENT = 20;
1893 
1894     /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT =
1895         new Directions(new int[] { 0, RUN_LENGTH_MASK });
1896     /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT =
1897         new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG });
1898 
1899     /* package */ static final char[] ELLIPSIS_NORMAL = { '\u2026' }; // this is "..."
1900     /* package */ static final char[] ELLIPSIS_TWO_DOTS = { '\u2025' }; // this is ".."
1901 }
1902