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