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