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