Home | History | Annotate | Download | only in text
      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     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     /* package */ static final int DIR_REQUEST_LTR = 1;
   1941     /* package */ static final int DIR_REQUEST_RTL = -1;
   1942     /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2;
   1943     /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2;
   1944 
   1945     public enum Alignment {
   1946         ALIGN_NORMAL,
   1947         ALIGN_OPPOSITE,
   1948         ALIGN_CENTER,
   1949         // XXX ALIGN_LEFT,
   1950         // XXX ALIGN_RIGHT,
   1951     }
   1952 
   1953     private static final int TAB_INCREMENT = 20;
   1954 
   1955     /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT =
   1956                                        new Directions(new short[] { 32767 });
   1957     /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT =
   1958                                        new Directions(new short[] { 0, 32767 });
   1959 
   1960 }
   1961 
   1962