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