Layout.java revision 71b8dd71e49016e057c46a257f79162d186a3c3a
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        int best = min;
753        float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz);
754
755        int here = min;
756        for (int i = 0; i < dirs.mDirections.length; i++) {
757            int there = here + dirs.mDirections[i];
758            int swap = ((i & 1) == 0) ? 1 : -1;
759
760            if (there > max)
761                there = max;
762
763            int high = there - 1 + 1, low = here + 1 - 1, guess;
764
765            while (high - low > 1) {
766                guess = (high + low) / 2;
767                int adguess = getOffsetAtStartOf(guess);
768
769                if (getPrimaryHorizontal(adguess) * swap >= horiz * swap)
770                    high = guess;
771                else
772                    low = guess;
773            }
774
775            if (low < here + 1)
776                low = here + 1;
777
778            if (low < there) {
779                low = getOffsetAtStartOf(low);
780
781                float dist = Math.abs(getPrimaryHorizontal(low) - horiz);
782
783                int aft = TextUtils.getOffsetAfter(mText, low);
784                if (aft < there) {
785                    float other = Math.abs(getPrimaryHorizontal(aft) - horiz);
786
787                    if (other < dist) {
788                        dist = other;
789                        low = aft;
790                    }
791                }
792
793                if (dist < bestdist) {
794                    bestdist = dist;
795                    best = low;
796                }
797            }
798
799            float dist = Math.abs(getPrimaryHorizontal(here) - horiz);
800
801            if (dist < bestdist) {
802                bestdist = dist;
803                best = here;
804            }
805
806            here = there;
807        }
808
809        float dist = Math.abs(getPrimaryHorizontal(max) - horiz);
810
811        if (dist < bestdist) {
812            bestdist = dist;
813            best = max;
814        }
815
816        return best;
817    }
818
819    /**
820     * Return the text offset after the last character on the specified line.
821     */
822    public final int getLineEnd(int line) {
823        return getLineStart(line + 1);
824    }
825
826    /**
827     * Return the text offset after the last visible character (so whitespace
828     * is not counted) on the specified line.
829     */
830    public int getLineVisibleEnd(int line) {
831        return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1));
832    }
833
834    private int getLineVisibleEnd(int line, int start, int end) {
835        if (DEBUG) {
836            Assert.assertTrue(getLineStart(line) == start && getLineStart(line+1) == end);
837        }
838
839        CharSequence text = mText;
840        char ch;
841        if (line == getLineCount() - 1) {
842            return end;
843        }
844
845        for (; end > start; end--) {
846            ch = text.charAt(end - 1);
847
848            if (ch == '\n') {
849                return end - 1;
850            }
851
852            if (ch != ' ' && ch != '\t') {
853                break;
854            }
855
856        }
857
858        return end;
859    }
860
861    /**
862     * Return the vertical position of the bottom of the specified line.
863     */
864    public final int getLineBottom(int line) {
865        return getLineTop(line + 1);
866    }
867
868    /**
869     * Return the vertical position of the baseline of the specified line.
870     */
871    public final int getLineBaseline(int line) {
872        // getLineTop(line+1) == getLineTop(line)
873        return getLineTop(line+1) - getLineDescent(line);
874    }
875
876    /**
877     * Get the ascent of the text on the specified line.
878     * The return value is negative to match the Paint.ascent() convention.
879     */
880    public final int getLineAscent(int line) {
881        // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line)
882        return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line));
883    }
884
885    /**
886     * Return the text offset that would be reached by moving left
887     * (possibly onto another line) from the specified offset.
888     */
889    public int getOffsetToLeftOf(int offset) {
890        int line = getLineForOffset(offset);
891        int start = getLineStart(line);
892        int end = getLineEnd(line);
893        Directions dirs = getLineDirections(line);
894
895        if (line != getLineCount() - 1)
896            end--;
897
898        float horiz = getPrimaryHorizontal(offset);
899
900        int best = offset;
901        float besth = Integer.MIN_VALUE;
902        int candidate;
903
904        candidate = TextUtils.getOffsetBefore(mText, offset);
905        if (candidate >= start && candidate <= end) {
906            float h = getPrimaryHorizontal(candidate);
907
908            if (h < horiz && h > besth) {
909                best = candidate;
910                besth = h;
911            }
912        }
913
914        candidate = TextUtils.getOffsetAfter(mText, offset);
915        if (candidate >= start && candidate <= end) {
916            float h = getPrimaryHorizontal(candidate);
917
918            if (h < horiz && h > besth) {
919                best = candidate;
920                besth = h;
921            }
922        }
923
924        int here = start;
925        for (int i = 0; i < dirs.mDirections.length; i++) {
926            int there = here + dirs.mDirections[i];
927            if (there > end)
928                there = end;
929
930            float h = getPrimaryHorizontal(here);
931
932            if (h < horiz && h > besth) {
933                best = here;
934                besth = h;
935            }
936
937            candidate = TextUtils.getOffsetAfter(mText, here);
938            if (candidate >= start && candidate <= end) {
939                h = getPrimaryHorizontal(candidate);
940
941                if (h < horiz && h > besth) {
942                    best = candidate;
943                    besth = h;
944                }
945            }
946
947            candidate = TextUtils.getOffsetBefore(mText, there);
948            if (candidate >= start && candidate <= end) {
949                h = getPrimaryHorizontal(candidate);
950
951                if (h < horiz && h > besth) {
952                    best = candidate;
953                    besth = h;
954                }
955            }
956
957            here = there;
958        }
959
960        float h = getPrimaryHorizontal(end);
961
962        if (h < horiz && h > besth) {
963            best = end;
964            besth = h;
965        }
966
967        if (best != offset)
968            return best;
969
970        int dir = getParagraphDirection(line);
971
972        if (dir > 0) {
973            if (line == 0)
974                return best;
975            else
976                return getOffsetForHorizontal(line - 1, 10000);
977        } else {
978            if (line == getLineCount() - 1)
979                return best;
980            else
981                return getOffsetForHorizontal(line + 1, 10000);
982        }
983    }
984
985    /**
986     * Return the text offset that would be reached by moving right
987     * (possibly onto another line) from the specified offset.
988     */
989    public int getOffsetToRightOf(int offset) {
990        int line = getLineForOffset(offset);
991        int start = getLineStart(line);
992        int end = getLineEnd(line);
993        Directions dirs = getLineDirections(line);
994
995        if (line != getLineCount() - 1)
996            end--;
997
998        float horiz = getPrimaryHorizontal(offset);
999
1000        int best = offset;
1001        float besth = Integer.MAX_VALUE;
1002        int candidate;
1003
1004        candidate = TextUtils.getOffsetBefore(mText, offset);
1005        if (candidate >= start && candidate <= end) {
1006            float h = getPrimaryHorizontal(candidate);
1007
1008            if (h > horiz && h < besth) {
1009                best = candidate;
1010                besth = h;
1011            }
1012        }
1013
1014        candidate = TextUtils.getOffsetAfter(mText, offset);
1015        if (candidate >= start && candidate <= end) {
1016            float h = getPrimaryHorizontal(candidate);
1017
1018            if (h > horiz && h < besth) {
1019                best = candidate;
1020                besth = h;
1021            }
1022        }
1023
1024        int here = start;
1025        for (int i = 0; i < dirs.mDirections.length; i++) {
1026            int there = here + dirs.mDirections[i];
1027            if (there > end)
1028                there = end;
1029
1030            float h = getPrimaryHorizontal(here);
1031
1032            if (h > horiz && h < besth) {
1033                best = here;
1034                besth = h;
1035            }
1036
1037            candidate = TextUtils.getOffsetAfter(mText, here);
1038            if (candidate >= start && candidate <= end) {
1039                h = getPrimaryHorizontal(candidate);
1040
1041                if (h > horiz && h < besth) {
1042                    best = candidate;
1043                    besth = h;
1044                }
1045            }
1046
1047            candidate = TextUtils.getOffsetBefore(mText, there);
1048            if (candidate >= start && candidate <= end) {
1049                h = getPrimaryHorizontal(candidate);
1050
1051                if (h > horiz && h < besth) {
1052                    best = candidate;
1053                    besth = h;
1054                }
1055            }
1056
1057            here = there;
1058        }
1059
1060        float h = getPrimaryHorizontal(end);
1061
1062        if (h > horiz && h < besth) {
1063            best = end;
1064            besth = h;
1065        }
1066
1067        if (best != offset)
1068            return best;
1069
1070        int dir = getParagraphDirection(line);
1071
1072        if (dir > 0) {
1073            if (line == getLineCount() - 1)
1074                return best;
1075            else
1076                return getOffsetForHorizontal(line + 1, -10000);
1077        } else {
1078            if (line == 0)
1079                return best;
1080            else
1081                return getOffsetForHorizontal(line - 1, -10000);
1082        }
1083    }
1084
1085    private int getOffsetAtStartOf(int offset) {
1086        if (offset == 0)
1087            return 0;
1088
1089        CharSequence text = mText;
1090        char c = text.charAt(offset);
1091
1092        if (c >= '\uDC00' && c <= '\uDFFF') {
1093            char c1 = text.charAt(offset - 1);
1094
1095            if (c1 >= '\uD800' && c1 <= '\uDBFF')
1096                offset -= 1;
1097        }
1098
1099        if (mSpannedText) {
1100            ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
1101                                                       ReplacementSpan.class);
1102
1103            for (int i = 0; i < spans.length; i++) {
1104                int start = ((Spanned) text).getSpanStart(spans[i]);
1105                int end = ((Spanned) text).getSpanEnd(spans[i]);
1106
1107                if (start < offset && end > offset)
1108                    offset = start;
1109            }
1110        }
1111
1112        return offset;
1113    }
1114
1115    /**
1116     * Fills in the specified Path with a representation of a cursor
1117     * at the specified offset.  This will often be a vertical line
1118     * but can be multiple discontinous lines in text with multiple
1119     * directionalities.
1120     */
1121    public void getCursorPath(int point, Path dest,
1122                              CharSequence editingBuffer) {
1123        dest.reset();
1124
1125        int line = getLineForOffset(point);
1126        int top = getLineTop(line);
1127        int bottom = getLineTop(line+1);
1128
1129        float h1 = getPrimaryHorizontal(point) - 0.5f;
1130        float h2 = getSecondaryHorizontal(point) - 0.5f;
1131
1132        int caps = TextKeyListener.getMetaState(editingBuffer,
1133                                                KeyEvent.META_SHIFT_ON) |
1134                   TextKeyListener.getMetaState(editingBuffer,
1135                                                TextKeyListener.META_SELECTING);
1136        int fn = TextKeyListener.getMetaState(editingBuffer,
1137                                              KeyEvent.META_ALT_ON);
1138        int dist = 0;
1139
1140        if (caps != 0 || fn != 0) {
1141            dist = (bottom - top) >> 2;
1142
1143            if (fn != 0)
1144                top += dist;
1145            if (caps != 0)
1146                bottom -= dist;
1147        }
1148
1149        if (h1 < 0.5f)
1150            h1 = 0.5f;
1151        if (h2 < 0.5f)
1152            h2 = 0.5f;
1153
1154        if (h1 == h2) {
1155            dest.moveTo(h1, top);
1156            dest.lineTo(h1, bottom);
1157        } else {
1158            dest.moveTo(h1, top);
1159            dest.lineTo(h1, (top + bottom) >> 1);
1160
1161            dest.moveTo(h2, (top + bottom) >> 1);
1162            dest.lineTo(h2, bottom);
1163        }
1164
1165        if (caps == 2) {
1166            dest.moveTo(h2, bottom);
1167            dest.lineTo(h2 - dist, bottom + dist);
1168            dest.lineTo(h2, bottom);
1169            dest.lineTo(h2 + dist, bottom + dist);
1170        } else if (caps == 1) {
1171            dest.moveTo(h2, bottom);
1172            dest.lineTo(h2 - dist, bottom + dist);
1173
1174            dest.moveTo(h2 - dist, bottom + dist - 0.5f);
1175            dest.lineTo(h2 + dist, bottom + dist - 0.5f);
1176
1177            dest.moveTo(h2 + dist, bottom + dist);
1178            dest.lineTo(h2, bottom);
1179        }
1180
1181        if (fn == 2) {
1182            dest.moveTo(h1, top);
1183            dest.lineTo(h1 - dist, top - dist);
1184            dest.lineTo(h1, top);
1185            dest.lineTo(h1 + dist, top - dist);
1186        } else if (fn == 1) {
1187            dest.moveTo(h1, top);
1188            dest.lineTo(h1 - dist, top - dist);
1189
1190            dest.moveTo(h1 - dist, top - dist + 0.5f);
1191            dest.lineTo(h1 + dist, top - dist + 0.5f);
1192
1193            dest.moveTo(h1 + dist, top - dist);
1194            dest.lineTo(h1, top);
1195        }
1196    }
1197
1198    private void addSelection(int line, int start, int end,
1199                              int top, int bottom, Path dest) {
1200        int linestart = getLineStart(line);
1201        int lineend = getLineEnd(line);
1202        Directions dirs = getLineDirections(line);
1203
1204        if (lineend > linestart && mText.charAt(lineend - 1) == '\n')
1205            lineend--;
1206
1207        int here = linestart;
1208        for (int i = 0; i < dirs.mDirections.length; i++) {
1209            int there = here + dirs.mDirections[i];
1210            if (there > lineend)
1211                there = lineend;
1212
1213            if (start <= there && end >= here) {
1214                int st = Math.max(start, here);
1215                int en = Math.min(end, there);
1216
1217                if (st != en) {
1218                    float h1 = getHorizontal(st, false, false, line);
1219                    float h2 = getHorizontal(en, true, false, line);
1220
1221                    dest.addRect(h1, top, h2, bottom, Path.Direction.CW);
1222                }
1223            }
1224
1225            here = there;
1226        }
1227    }
1228
1229    /**
1230     * Fills in the specified Path with a representation of a highlight
1231     * between the specified offsets.  This will often be a rectangle
1232     * or a potentially discontinuous set of rectangles.  If the start
1233     * and end are the same, the returned path is empty.
1234     */
1235    public void getSelectionPath(int start, int end, Path dest) {
1236        dest.reset();
1237
1238        if (start == end)
1239            return;
1240
1241        if (end < start) {
1242            int temp = end;
1243            end = start;
1244            start = temp;
1245        }
1246
1247        int startline = getLineForOffset(start);
1248        int endline = getLineForOffset(end);
1249
1250        int top = getLineTop(startline);
1251        int bottom = getLineBottom(endline);
1252
1253        if (startline == endline) {
1254            addSelection(startline, start, end, top, bottom, dest);
1255        } else {
1256            final float width = mWidth;
1257
1258            addSelection(startline, start, getLineEnd(startline),
1259                         top, getLineBottom(startline), dest);
1260
1261            if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT)
1262                dest.addRect(getLineLeft(startline), top,
1263                              0, getLineBottom(startline), Path.Direction.CW);
1264            else
1265                dest.addRect(getLineRight(startline), top,
1266                              width, getLineBottom(startline), Path.Direction.CW);
1267
1268            for (int i = startline + 1; i < endline; i++) {
1269                top = getLineTop(i);
1270                bottom = getLineBottom(i);
1271                dest.addRect(0, top, width, bottom, Path.Direction.CW);
1272            }
1273
1274            top = getLineTop(endline);
1275            bottom = getLineBottom(endline);
1276
1277            addSelection(endline, getLineStart(endline), end,
1278                         top, bottom, dest);
1279
1280            if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT)
1281                dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW);
1282            else
1283                dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW);
1284        }
1285    }
1286
1287    /**
1288     * Get the alignment of the specified paragraph, taking into account
1289     * markup attached to it.
1290     */
1291    public final Alignment getParagraphAlignment(int line) {
1292        Alignment align = mAlignment;
1293
1294        if (mSpannedText) {
1295            Spanned sp = (Spanned) mText;
1296            AlignmentSpan[] spans = sp.getSpans(getLineStart(line),
1297                                                getLineEnd(line),
1298                                                AlignmentSpan.class);
1299
1300            int spanLength = spans.length;
1301            if (spanLength > 0) {
1302                align = spans[spanLength-1].getAlignment();
1303            }
1304        }
1305
1306        return align;
1307    }
1308
1309    /**
1310     * Get the left edge of the specified paragraph, inset by left margins.
1311     */
1312    public final int getParagraphLeft(int line) {
1313        int dir = getParagraphDirection(line);
1314
1315        int left = 0;
1316
1317        boolean par = false;
1318        int off = getLineStart(line);
1319        if (off == 0 || mText.charAt(off - 1) == '\n')
1320            par = true;
1321
1322        if (dir == DIR_LEFT_TO_RIGHT) {
1323            if (mSpannedText) {
1324                Spanned sp = (Spanned) mText;
1325                LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line),
1326                                                        getLineEnd(line),
1327                                                        LeadingMarginSpan.class);
1328
1329                for (int i = 0; i < spans.length; i++) {
1330                    boolean margin = par;
1331                    LeadingMarginSpan span = spans[i];
1332                    if (span instanceof LeadingMarginSpan.LeadingMarginSpan2) {
1333                        int count = ((LeadingMarginSpan.LeadingMarginSpan2)span).getLeadingMarginLineCount();
1334                        margin = count >= line;
1335                    }
1336                    left += span.getLeadingMargin(margin);
1337                }
1338            }
1339        }
1340
1341        return left;
1342    }
1343
1344    /**
1345     * Get the right edge of the specified paragraph, inset by right margins.
1346     */
1347    public final int getParagraphRight(int line) {
1348        int dir = getParagraphDirection(line);
1349
1350        int right = mWidth;
1351
1352        boolean par = false;
1353        int off = getLineStart(line);
1354        if (off == 0 || mText.charAt(off - 1) == '\n')
1355            par = true;
1356
1357
1358        if (dir == DIR_RIGHT_TO_LEFT) {
1359            if (mSpannedText) {
1360                Spanned sp = (Spanned) mText;
1361                LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line),
1362                                                        getLineEnd(line),
1363                                                        LeadingMarginSpan.class);
1364
1365                for (int i = 0; i < spans.length; i++) {
1366                    right -= spans[i].getLeadingMargin(par);
1367                }
1368            }
1369        }
1370
1371        return right;
1372    }
1373
1374    private void drawText(Canvas canvas,
1375                                 CharSequence text, int start, int end,
1376                                 int dir, Directions directions,
1377                                 float x, int top, int y, int bottom,
1378                                 TextPaint paint,
1379                                 TextPaint workPaint,
1380                                 boolean hasTabs, Object[] parspans) {
1381        char[] buf;
1382        if (!hasTabs) {
1383            if (directions == DIRS_ALL_LEFT_TO_RIGHT) {
1384                if (DEBUG) {
1385                    Assert.assertTrue(DIR_LEFT_TO_RIGHT == dir);
1386                }
1387                Styled.drawText(canvas, text, start, end, dir, false, x, top, y, bottom, paint, workPaint, false);
1388                return;
1389            }
1390            buf = null;
1391        } else {
1392            buf = TextUtils.obtain(end - start);
1393            TextUtils.getChars(text, start, end, buf, 0);
1394        }
1395
1396        float h = 0;
1397
1398        int here = 0;
1399        for (int i = 0; i < directions.mDirections.length; i++) {
1400            int there = here + directions.mDirections[i];
1401            if (there > end - start)
1402                there = end - start;
1403
1404            int segstart = here;
1405            for (int j = hasTabs ? here : there; j <= there; j++) {
1406                if (j == there || buf[j] == '\t') {
1407                    h += Styled.drawText(canvas, text,
1408                                         start + segstart, start + j,
1409                                         dir, (i & 1) != 0, x + h,
1410                                         top, y, bottom, paint, workPaint,
1411                                         start + j != end);
1412
1413                    if (j != there && buf[j] == '\t')
1414                        h = dir * nextTab(text, start, end, h * dir, parspans);
1415
1416                    segstart = j + 1;
1417                } else if (hasTabs && buf[j] >= 0xD800 && buf[j] <= 0xDFFF && j + 1 < there) {
1418                    int emoji = Character.codePointAt(buf, j);
1419
1420                    if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) {
1421                        Bitmap bm = EMOJI_FACTORY.
1422                            getBitmapFromAndroidPua(emoji);
1423
1424                        if (bm != null) {
1425                            h += Styled.drawText(canvas, text,
1426                                                 start + segstart, start + j,
1427                                                 dir, (i & 1) != 0, x + h,
1428                                                 top, y, bottom, paint, workPaint,
1429                                                 start + j != end);
1430
1431                            if (mEmojiRect == null) {
1432                                mEmojiRect = new RectF();
1433                            }
1434
1435                            workPaint.set(paint);
1436                            Styled.measureText(paint, workPaint, text,
1437                                               start + j, start + j + 1,
1438                                               null);
1439
1440                            float bitmapHeight = bm.getHeight();
1441                            float textHeight = -workPaint.ascent();
1442                            float scale = textHeight / bitmapHeight;
1443                            float width = bm.getWidth() * scale;
1444
1445                            mEmojiRect.set(x + h, y - textHeight,
1446                                           x + h + width, y);
1447
1448                            canvas.drawBitmap(bm, null, mEmojiRect, paint);
1449                            h += width;
1450
1451                            j++;
1452                            segstart = j + 1;
1453                        }
1454                    }
1455                }
1456            }
1457
1458            here = there;
1459        }
1460
1461        if (hasTabs)
1462            TextUtils.recycle(buf);
1463    }
1464
1465    private static float measureText(TextPaint paint,
1466                                     TextPaint workPaint,
1467                                     CharSequence text,
1468                                     int start, int offset, int end,
1469                                     int dir, Directions directions,
1470                                     boolean trailing, boolean alt,
1471                                     boolean hasTabs, Object[] tabs) {
1472        char[] buf = null;
1473
1474        if (hasTabs) {
1475            buf = TextUtils.obtain(end - start);
1476            TextUtils.getChars(text, start, end, buf, 0);
1477        }
1478
1479        float h = 0;
1480
1481        if (alt) {
1482            if (dir == DIR_RIGHT_TO_LEFT)
1483                trailing = !trailing;
1484        }
1485
1486        int here = 0;
1487        for (int i = 0; i < directions.mDirections.length; i++) {
1488            if (alt)
1489                trailing = !trailing;
1490
1491            int there = here + directions.mDirections[i];
1492            if (there > end - start)
1493                there = end - start;
1494
1495            int segstart = here;
1496            for (int j = hasTabs ? here : there; j <= there; j++) {
1497                int codept = 0;
1498                Bitmap bm = null;
1499
1500                if (hasTabs && j < there) {
1501                    codept = buf[j];
1502                }
1503
1504                if (codept >= 0xD800 && codept <= 0xDFFF && j + 1 < there) {
1505                    codept = Character.codePointAt(buf, j);
1506
1507                    if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) {
1508                        bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
1509                    }
1510                }
1511
1512                if (j == there || codept == '\t' || bm != null) {
1513                    float segw;
1514
1515                    if (offset < start + j ||
1516                       (trailing && offset <= start + j)) {
1517                        if (dir == DIR_LEFT_TO_RIGHT && (i & 1) == 0) {
1518                            h += Styled.measureText(paint, workPaint, text,
1519                                                    start + segstart, offset,
1520                                                    null);
1521                            return h;
1522                        }
1523
1524                        if (dir == DIR_RIGHT_TO_LEFT && (i & 1) != 0) {
1525                            h -= Styled.measureText(paint, workPaint, text,
1526                                                    start + segstart, offset,
1527                                                    null);
1528                            return h;
1529                        }
1530                    }
1531
1532                    segw = Styled.measureText(paint, workPaint, text,
1533                                              start + segstart, start + j,
1534                                              null);
1535
1536                    if (offset < start + j ||
1537                        (trailing && offset <= start + j)) {
1538                        if (dir == DIR_LEFT_TO_RIGHT) {
1539                            h += segw - Styled.measureText(paint, workPaint,
1540                                                           text,
1541                                                           start + segstart,
1542                                                           offset, null);
1543                            return h;
1544                        }
1545
1546                        if (dir == DIR_RIGHT_TO_LEFT) {
1547                            h -= segw - Styled.measureText(paint, workPaint,
1548                                                           text,
1549                                                           start + segstart,
1550                                                           offset, null);
1551                            return h;
1552                        }
1553                    }
1554
1555                    if (dir == DIR_RIGHT_TO_LEFT)
1556                        h -= segw;
1557                    else
1558                        h += segw;
1559
1560                    if (j != there && buf[j] == '\t') {
1561                        if (offset == start + j)
1562                            return h;
1563
1564                        h = dir * nextTab(text, start, end, h * dir, tabs);
1565                    }
1566
1567                    if (bm != null) {
1568                        workPaint.set(paint);
1569                        Styled.measureText(paint, workPaint, text,
1570                                           j, j + 2, null);
1571
1572                        float wid = (float) bm.getWidth() *
1573                                    -workPaint.ascent() / bm.getHeight();
1574
1575                        if (dir == DIR_RIGHT_TO_LEFT) {
1576                            h -= wid;
1577                        } else {
1578                            h += wid;
1579                        }
1580
1581                        j++;
1582                    }
1583
1584                    segstart = j + 1;
1585                }
1586            }
1587
1588            here = there;
1589        }
1590
1591        if (hasTabs)
1592            TextUtils.recycle(buf);
1593
1594        return h;
1595    }
1596
1597    /**
1598     * Measure width of a run of text on a single line that is known to all be
1599     * in the same direction as the paragraph base direction. Returns the width,
1600     * and the line metrics in fm if fm is not null.
1601     *
1602     * @param paint the paint for the text; will not be modified
1603     * @param workPaint paint available for modification
1604     * @param text text
1605     * @param start start of the line
1606     * @param end limit of the line
1607     * @param fm object to return integer metrics in, can be null
1608     * @param hasTabs true if it is known that the line has tabs
1609     * @param tabs tab position information
1610     * @return the width of the text from start to end
1611     */
1612    /* package */ static float measureText(TextPaint paint,
1613                                           TextPaint workPaint,
1614                                           CharSequence text,
1615                                           int start, int end,
1616                                           Paint.FontMetricsInt fm,
1617                                           boolean hasTabs, Object[] tabs) {
1618        char[] buf = null;
1619
1620        if (hasTabs) {
1621            buf = TextUtils.obtain(end - start);
1622            TextUtils.getChars(text, start, end, buf, 0);
1623        }
1624
1625        int len = end - start;
1626
1627        int lastPos = 0;
1628        float width = 0;
1629        int ascent = 0, descent = 0, top = 0, bottom = 0;
1630
1631        if (fm != null) {
1632            fm.ascent = 0;
1633            fm.descent = 0;
1634        }
1635
1636        for (int pos = hasTabs ? 0 : len; pos <= len; pos++) {
1637            int codept = 0;
1638            Bitmap bm = null;
1639
1640            if (hasTabs && pos < len) {
1641                codept = buf[pos];
1642            }
1643
1644            if (codept >= 0xD800 && codept <= 0xDFFF && pos < len) {
1645                codept = Character.codePointAt(buf, pos);
1646
1647                if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) {
1648                    bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
1649                }
1650            }
1651
1652            if (pos == len || codept == '\t' || bm != null) {
1653                workPaint.baselineShift = 0;
1654
1655                width += Styled.measureText(paint, workPaint, text,
1656                                        start + lastPos, start + pos,
1657                                        fm);
1658
1659                if (fm != null) {
1660                    if (workPaint.baselineShift < 0) {
1661                        fm.ascent += workPaint.baselineShift;
1662                        fm.top += workPaint.baselineShift;
1663                    } else {
1664                        fm.descent += workPaint.baselineShift;
1665                        fm.bottom += workPaint.baselineShift;
1666                    }
1667                }
1668
1669                if (pos != len) {
1670                    if (bm == null) {
1671                        // no emoji, must have hit a tab
1672                        width = nextTab(text, start, end, width, tabs);
1673                    } else {
1674                        // This sets up workPaint with the font on the emoji
1675                        // text, so that we can extract the ascent and scale.
1676
1677                        // We can't use the result of the previous call to
1678                        // measureText because the emoji might have its own style.
1679                        // We have to initialize workPaint here because if the
1680                        // text is unstyled measureText might not use workPaint
1681                        // at all.
1682                        workPaint.set(paint);
1683                        Styled.measureText(paint, workPaint, text,
1684                                           start + pos, start + pos + 1, null);
1685
1686                        width += (float) bm.getWidth() *
1687                                    -workPaint.ascent() / bm.getHeight();
1688
1689                        // Since we had an emoji, we bump past the second half
1690                        // of the surrogate pair.
1691                        pos++;
1692                    }
1693                }
1694
1695                if (fm != null) {
1696                    if (fm.ascent < ascent) {
1697                        ascent = fm.ascent;
1698                    }
1699                    if (fm.descent > descent) {
1700                        descent = fm.descent;
1701                    }
1702
1703                    if (fm.top < top) {
1704                        top = fm.top;
1705                    }
1706                    if (fm.bottom > bottom) {
1707                        bottom = fm.bottom;
1708                    }
1709
1710                    // No need to take bitmap height into account here,
1711                    // since it is scaled to match the text height.
1712                }
1713
1714                lastPos = pos + 1;
1715            }
1716        }
1717
1718        if (fm != null) {
1719            fm.ascent = ascent;
1720            fm.descent = descent;
1721            fm.top = top;
1722            fm.bottom = bottom;
1723        }
1724
1725        if (hasTabs)
1726            TextUtils.recycle(buf);
1727
1728        return width;
1729    }
1730
1731    /**
1732     * Returns the position of the next tab stop after h on the line.
1733     *
1734     * @param text the text
1735     * @param start start of the line
1736     * @param end limit of the line
1737     * @param h the current horizontal offset
1738     * @param tabs the tabs, can be null.  If it is null, any tabs in effect
1739     * on the line will be used.  If there are no tabs, a default offset
1740     * will be used to compute the tab stop.
1741     * @return the offset of the next tab stop.
1742     */
1743    /* package */ static float nextTab(CharSequence text, int start, int end,
1744                                       float h, Object[] tabs) {
1745        float nh = Float.MAX_VALUE;
1746        boolean alltabs = false;
1747
1748        if (text instanceof Spanned) {
1749            if (tabs == null) {
1750                tabs = ((Spanned) text).getSpans(start, end, TabStopSpan.class);
1751                alltabs = true;
1752            }
1753
1754            for (int i = 0; i < tabs.length; i++) {
1755                if (!alltabs) {
1756                    if (!(tabs[i] instanceof TabStopSpan))
1757                        continue;
1758                }
1759
1760                int where = ((TabStopSpan) tabs[i]).getTabStop();
1761
1762                if (where < nh && where > h)
1763                    nh = where;
1764            }
1765
1766            if (nh != Float.MAX_VALUE)
1767                return nh;
1768        }
1769
1770        return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT;
1771    }
1772
1773    protected final boolean isSpanned() {
1774        return mSpannedText;
1775    }
1776
1777    private void ellipsize(int start, int end, int line,
1778                           char[] dest, int destoff) {
1779        int ellipsisCount = getEllipsisCount(line);
1780
1781        if (ellipsisCount == 0) {
1782            return;
1783        }
1784
1785        int ellipsisStart = getEllipsisStart(line);
1786        int linestart = getLineStart(line);
1787
1788        for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) {
1789            char c;
1790
1791            if (i == ellipsisStart) {
1792                c = '\u2026'; // ellipsis
1793            } else {
1794                c = '\uFEFF'; // 0-width space
1795            }
1796
1797            int a = i + linestart;
1798
1799            if (a >= start && a < end) {
1800                dest[destoff + a - start] = c;
1801            }
1802        }
1803    }
1804
1805    /**
1806     * Stores information about bidirectional (left-to-right or right-to-left)
1807     * text within the layout of a line.  TODO: This work is not complete
1808     * or correct and will be fleshed out in a later revision.
1809     */
1810    public static class Directions {
1811        private short[] mDirections;
1812
1813        // The values in mDirections are the offsets from the first character
1814        // in the line to the next flip in direction.  Runs at even indices
1815        // are left-to-right, the others are right-to-left.  So, for example,
1816        // a line that starts with a right-to-left run has 0 at mDirections[0],
1817        // since the 'first' (ltr) run is zero length.
1818        //
1819        // The code currently assumes that each run is adjacent to the previous
1820        // one, progressing in the base line direction.  This isn't sufficient
1821        // to handle nested runs, for example numeric text in an rtl context
1822        // in an ltr paragraph.
1823        /* package */ Directions(short[] dirs) {
1824            mDirections = dirs;
1825        }
1826    }
1827
1828    /**
1829     * Return the offset of the first character to be ellipsized away,
1830     * relative to the start of the line.  (So 0 if the beginning of the
1831     * line is ellipsized, not getLineStart().)
1832     */
1833    public abstract int getEllipsisStart(int line);
1834    /**
1835     * Returns the number of characters to be ellipsized away, or 0 if
1836     * no ellipsis is to take place.
1837     */
1838    public abstract int getEllipsisCount(int line);
1839
1840    /* package */ static class Ellipsizer implements CharSequence, GetChars {
1841        /* package */ CharSequence mText;
1842        /* package */ Layout mLayout;
1843        /* package */ int mWidth;
1844        /* package */ TextUtils.TruncateAt mMethod;
1845
1846        public Ellipsizer(CharSequence s) {
1847            mText = s;
1848        }
1849
1850        public char charAt(int off) {
1851            char[] buf = TextUtils.obtain(1);
1852            getChars(off, off + 1, buf, 0);
1853            char ret = buf[0];
1854
1855            TextUtils.recycle(buf);
1856            return ret;
1857        }
1858
1859        public void getChars(int start, int end, char[] dest, int destoff) {
1860            int line1 = mLayout.getLineForOffset(start);
1861            int line2 = mLayout.getLineForOffset(end);
1862
1863            TextUtils.getChars(mText, start, end, dest, destoff);
1864
1865            for (int i = line1; i <= line2; i++) {
1866                mLayout.ellipsize(start, end, i, dest, destoff);
1867            }
1868        }
1869
1870        public int length() {
1871            return mText.length();
1872        }
1873
1874        public CharSequence subSequence(int start, int end) {
1875            char[] s = new char[end - start];
1876            getChars(start, end, s, 0);
1877            return new String(s);
1878        }
1879
1880        public String toString() {
1881            char[] s = new char[length()];
1882            getChars(0, length(), s, 0);
1883            return new String(s);
1884        }
1885
1886    }
1887
1888    /* package */ static class SpannedEllipsizer
1889                    extends Ellipsizer implements Spanned {
1890        private Spanned mSpanned;
1891
1892        public SpannedEllipsizer(CharSequence display) {
1893            super(display);
1894            mSpanned = (Spanned) display;
1895        }
1896
1897        public <T> T[] getSpans(int start, int end, Class<T> type) {
1898            return mSpanned.getSpans(start, end, type);
1899        }
1900
1901        public int getSpanStart(Object tag) {
1902            return mSpanned.getSpanStart(tag);
1903        }
1904
1905        public int getSpanEnd(Object tag) {
1906            return mSpanned.getSpanEnd(tag);
1907        }
1908
1909        public int getSpanFlags(Object tag) {
1910            return mSpanned.getSpanFlags(tag);
1911        }
1912
1913        public int nextSpanTransition(int start, int limit, Class type) {
1914            return mSpanned.nextSpanTransition(start, limit, type);
1915        }
1916
1917        public CharSequence subSequence(int start, int end) {
1918            char[] s = new char[end - start];
1919            getChars(start, end, s, 0);
1920
1921            SpannableString ss = new SpannableString(new String(s));
1922            TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0);
1923            return ss;
1924        }
1925    }
1926
1927    private CharSequence mText;
1928    private TextPaint mPaint;
1929    /* package */ TextPaint mWorkPaint;
1930    private int mWidth;
1931    private Alignment mAlignment = Alignment.ALIGN_NORMAL;
1932    private float mSpacingMult;
1933    private float mSpacingAdd;
1934    private static Rect sTempRect = new Rect();
1935    private boolean mSpannedText;
1936
1937    public static final int DIR_LEFT_TO_RIGHT = 1;
1938    public static final int DIR_RIGHT_TO_LEFT = -1;
1939
1940    public enum Alignment {
1941        ALIGN_NORMAL,
1942        ALIGN_OPPOSITE,
1943        ALIGN_CENTER,
1944        // XXX ALIGN_LEFT,
1945        // XXX ALIGN_RIGHT,
1946    }
1947
1948    private static final int TAB_INCREMENT = 20;
1949
1950    /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT =
1951                                       new Directions(new short[] { 32767 });
1952    /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT =
1953                                       new Directions(new short[] { 0, 32767 });
1954
1955}
1956
1957