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