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