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