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