Home | History | Annotate | Download | only in text
      1 /*
      2  * Copyright (C) 2010 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.graphics.Bitmap;
     20 import android.graphics.Canvas;
     21 import android.graphics.Paint;
     22 import android.graphics.Paint.FontMetricsInt;
     23 import android.graphics.RectF;
     24 import android.text.Layout.Directions;
     25 import android.text.Layout.TabStops;
     26 import android.text.style.CharacterStyle;
     27 import android.text.style.MetricAffectingSpan;
     28 import android.text.style.ReplacementSpan;
     29 import android.util.Log;
     30 
     31 import com.android.internal.util.ArrayUtils;
     32 
     33 import java.lang.reflect.Array;
     34 
     35 /**
     36  * Represents a line of styled text, for measuring in visual order and
     37  * for rendering.
     38  *
     39  * <p>Get a new instance using obtain(), and when finished with it, return it
     40  * to the pool using recycle().
     41  *
     42  * <p>Call set to prepare the instance for use, then either draw, measure,
     43  * metrics, or caretToLeftRightOf.
     44  *
     45  * @hide
     46  */
     47 class TextLine {
     48     private static final boolean DEBUG = false;
     49 
     50     private TextPaint mPaint;
     51     private CharSequence mText;
     52     private int mStart;
     53     private int mLen;
     54     private int mDir;
     55     private Directions mDirections;
     56     private boolean mHasTabs;
     57     private TabStops mTabs;
     58     private char[] mChars;
     59     private boolean mCharsValid;
     60     private Spanned mSpanned;
     61     private final TextPaint mWorkPaint = new TextPaint();
     62     private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =
     63             new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);
     64     private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =
     65             new SpanSet<CharacterStyle>(CharacterStyle.class);
     66     private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =
     67             new SpanSet<ReplacementSpan>(ReplacementSpan.class);
     68 
     69     private static final TextLine[] sCached = new TextLine[3];
     70 
     71     /**
     72      * Returns a new TextLine from the shared pool.
     73      *
     74      * @return an uninitialized TextLine
     75      */
     76     static TextLine obtain() {
     77         TextLine tl;
     78         synchronized (sCached) {
     79             for (int i = sCached.length; --i >= 0;) {
     80                 if (sCached[i] != null) {
     81                     tl = sCached[i];
     82                     sCached[i] = null;
     83                     return tl;
     84                 }
     85             }
     86         }
     87         tl = new TextLine();
     88         if (DEBUG) {
     89             Log.v("TLINE", "new: " + tl);
     90         }
     91         return tl;
     92     }
     93 
     94     /**
     95      * Puts a TextLine back into the shared pool. Do not use this TextLine once
     96      * it has been returned.
     97      * @param tl the textLine
     98      * @return null, as a convenience from clearing references to the provided
     99      * TextLine
    100      */
    101     static TextLine recycle(TextLine tl) {
    102         tl.mText = null;
    103         tl.mPaint = null;
    104         tl.mDirections = null;
    105 
    106         tl.mMetricAffectingSpanSpanSet.recycle();
    107         tl.mCharacterStyleSpanSet.recycle();
    108         tl.mReplacementSpanSpanSet.recycle();
    109 
    110         synchronized(sCached) {
    111             for (int i = 0; i < sCached.length; ++i) {
    112                 if (sCached[i] == null) {
    113                     sCached[i] = tl;
    114                     break;
    115                 }
    116             }
    117         }
    118         return null;
    119     }
    120 
    121     /**
    122      * Initializes a TextLine and prepares it for use.
    123      *
    124      * @param paint the base paint for the line
    125      * @param text the text, can be Styled
    126      * @param start the start of the line relative to the text
    127      * @param limit the limit of the line relative to the text
    128      * @param dir the paragraph direction of this line
    129      * @param directions the directions information of this line
    130      * @param hasTabs true if the line might contain tabs or emoji
    131      * @param tabStops the tabStops. Can be null.
    132      */
    133     void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
    134             Directions directions, boolean hasTabs, TabStops tabStops) {
    135         mPaint = paint;
    136         mText = text;
    137         mStart = start;
    138         mLen = limit - start;
    139         mDir = dir;
    140         mDirections = directions;
    141         if (mDirections == null) {
    142             throw new IllegalArgumentException("Directions cannot be null");
    143         }
    144         mHasTabs = hasTabs;
    145         mSpanned = null;
    146 
    147         boolean hasReplacement = false;
    148         if (text instanceof Spanned) {
    149             mSpanned = (Spanned) text;
    150             mReplacementSpanSpanSet.init(mSpanned, start, limit);
    151             hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
    152         }
    153 
    154         mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT;
    155 
    156         if (mCharsValid) {
    157             if (mChars == null || mChars.length < mLen) {
    158                 mChars = new char[ArrayUtils.idealCharArraySize(mLen)];
    159             }
    160             TextUtils.getChars(text, start, limit, mChars, 0);
    161             if (hasReplacement) {
    162                 // Handle these all at once so we don't have to do it as we go.
    163                 // Replace the first character of each replacement run with the
    164                 // object-replacement character and the remainder with zero width
    165                 // non-break space aka BOM.  Cursor movement code skips these
    166                 // zero-width characters.
    167                 char[] chars = mChars;
    168                 for (int i = start, inext; i < limit; i = inext) {
    169                     inext = mReplacementSpanSpanSet.getNextTransition(i, limit);
    170                     if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)) {
    171                         // transition into a span
    172                         chars[i - start] = '\ufffc';
    173                         for (int j = i - start + 1, e = inext - start; j < e; ++j) {
    174                             chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
    175                         }
    176                     }
    177                 }
    178             }
    179         }
    180         mTabs = tabStops;
    181     }
    182 
    183     /**
    184      * Renders the TextLine.
    185      *
    186      * @param c the canvas to render on
    187      * @param x the leading margin position
    188      * @param top the top of the line
    189      * @param y the baseline
    190      * @param bottom the bottom of the line
    191      */
    192     void draw(Canvas c, float x, int top, int y, int bottom) {
    193         if (!mHasTabs) {
    194             if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
    195                 drawRun(c, 0, mLen, false, x, top, y, bottom, false);
    196                 return;
    197             }
    198             if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
    199                 drawRun(c, 0, mLen, true, x, top, y, bottom, false);
    200                 return;
    201             }
    202         }
    203 
    204         float h = 0;
    205         int[] runs = mDirections.mDirections;
    206         RectF emojiRect = null;
    207 
    208         int lastRunIndex = runs.length - 2;
    209         for (int i = 0; i < runs.length; i += 2) {
    210             int runStart = runs[i];
    211             int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
    212             if (runLimit > mLen) {
    213                 runLimit = mLen;
    214             }
    215             boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
    216 
    217             int segstart = runStart;
    218             for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
    219                 int codept = 0;
    220                 Bitmap bm = null;
    221 
    222                 if (mHasTabs && j < runLimit) {
    223                     codept = mChars[j];
    224                     if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) {
    225                         codept = Character.codePointAt(mChars, j);
    226                         if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) {
    227                             bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
    228                         } else if (codept > 0xffff) {
    229                             ++j;
    230                             continue;
    231                         }
    232                     }
    233                 }
    234 
    235                 if (j == runLimit || codept == '\t' || bm != null) {
    236                     h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom,
    237                             i != lastRunIndex || j != mLen);
    238 
    239                     if (codept == '\t') {
    240                         h = mDir * nextTab(h * mDir);
    241                     } else if (bm != null) {
    242                         float bmAscent = ascent(j);
    243                         float bitmapHeight = bm.getHeight();
    244                         float scale = -bmAscent / bitmapHeight;
    245                         float width = bm.getWidth() * scale;
    246 
    247                         if (emojiRect == null) {
    248                             emojiRect = new RectF();
    249                         }
    250                         emojiRect.set(x + h, y + bmAscent,
    251                                 x + h + width, y);
    252                         c.drawBitmap(bm, null, emojiRect, mPaint);
    253                         h += width;
    254                         j++;
    255                     }
    256                     segstart = j + 1;
    257                 }
    258             }
    259         }
    260     }
    261 
    262     /**
    263      * Returns metrics information for the entire line.
    264      *
    265      * @param fmi receives font metrics information, can be null
    266      * @return the signed width of the line
    267      */
    268     float metrics(FontMetricsInt fmi) {
    269         return measure(mLen, false, fmi);
    270     }
    271 
    272     /**
    273      * Returns information about a position on the line.
    274      *
    275      * @param offset the line-relative character offset, between 0 and the
    276      * line length, inclusive
    277      * @param trailing true to measure the trailing edge of the character
    278      * before offset, false to measure the leading edge of the character
    279      * at offset.
    280      * @param fmi receives metrics information about the requested
    281      * character, can be null.
    282      * @return the signed offset from the leading margin to the requested
    283      * character edge.
    284      */
    285     float measure(int offset, boolean trailing, FontMetricsInt fmi) {
    286         int target = trailing ? offset - 1 : offset;
    287         if (target < 0) {
    288             return 0;
    289         }
    290 
    291         float h = 0;
    292 
    293         if (!mHasTabs) {
    294             if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
    295                 return measureRun(0, offset, mLen, false, fmi);
    296             }
    297             if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
    298                 return measureRun(0, offset, mLen, true, fmi);
    299             }
    300         }
    301 
    302         char[] chars = mChars;
    303         int[] runs = mDirections.mDirections;
    304         for (int i = 0; i < runs.length; i += 2) {
    305             int runStart = runs[i];
    306             int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
    307             if (runLimit > mLen) {
    308                 runLimit = mLen;
    309             }
    310             boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
    311 
    312             int segstart = runStart;
    313             for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
    314                 int codept = 0;
    315                 Bitmap bm = null;
    316 
    317                 if (mHasTabs && j < runLimit) {
    318                     codept = chars[j];
    319                     if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) {
    320                         codept = Character.codePointAt(chars, j);
    321                         if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) {
    322                             bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
    323                         } else if (codept > 0xffff) {
    324                             ++j;
    325                             continue;
    326                         }
    327                     }
    328                 }
    329 
    330                 if (j == runLimit || codept == '\t' || bm != null) {
    331                     boolean inSegment = target >= segstart && target < j;
    332 
    333                     boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
    334                     if (inSegment && advance) {
    335                         return h += measureRun(segstart, offset, j, runIsRtl, fmi);
    336                     }
    337 
    338                     float w = measureRun(segstart, j, j, runIsRtl, fmi);
    339                     h += advance ? w : -w;
    340 
    341                     if (inSegment) {
    342                         return h += measureRun(segstart, offset, j, runIsRtl, null);
    343                     }
    344 
    345                     if (codept == '\t') {
    346                         if (offset == j) {
    347                             return h;
    348                         }
    349                         h = mDir * nextTab(h * mDir);
    350                         if (target == j) {
    351                             return h;
    352                         }
    353                     }
    354 
    355                     if (bm != null) {
    356                         float bmAscent = ascent(j);
    357                         float wid = bm.getWidth() * -bmAscent / bm.getHeight();
    358                         h += mDir * wid;
    359                         j++;
    360                     }
    361 
    362                     segstart = j + 1;
    363                 }
    364             }
    365         }
    366 
    367         return h;
    368     }
    369 
    370     /**
    371      * Draws a unidirectional (but possibly multi-styled) run of text.
    372      *
    373      *
    374      * @param c the canvas to draw on
    375      * @param start the line-relative start
    376      * @param limit the line-relative limit
    377      * @param runIsRtl true if the run is right-to-left
    378      * @param x the position of the run that is closest to the leading margin
    379      * @param top the top of the line
    380      * @param y the baseline
    381      * @param bottom the bottom of the line
    382      * @param needWidth true if the width value is required.
    383      * @return the signed width of the run, based on the paragraph direction.
    384      * Only valid if needWidth is true.
    385      */
    386     private float drawRun(Canvas c, int start,
    387             int limit, boolean runIsRtl, float x, int top, int y, int bottom,
    388             boolean needWidth) {
    389 
    390         if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
    391             float w = -measureRun(start, limit, limit, runIsRtl, null);
    392             handleRun(start, limit, limit, runIsRtl, c, x + w, top,
    393                     y, bottom, null, false);
    394             return w;
    395         }
    396 
    397         return handleRun(start, limit, limit, runIsRtl, c, x, top,
    398                 y, bottom, null, needWidth);
    399     }
    400 
    401     /**
    402      * Measures a unidirectional (but possibly multi-styled) run of text.
    403      *
    404      *
    405      * @param start the line-relative start of the run
    406      * @param offset the offset to measure to, between start and limit inclusive
    407      * @param limit the line-relative limit of the run
    408      * @param runIsRtl true if the run is right-to-left
    409      * @param fmi receives metrics information about the requested
    410      * run, can be null.
    411      * @return the signed width from the start of the run to the leading edge
    412      * of the character at offset, based on the run (not paragraph) direction
    413      */
    414     private float measureRun(int start, int offset, int limit, boolean runIsRtl,
    415             FontMetricsInt fmi) {
    416         return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true);
    417     }
    418 
    419     /**
    420      * Walk the cursor through this line, skipping conjuncts and
    421      * zero-width characters.
    422      *
    423      * <p>This function cannot properly walk the cursor off the ends of the line
    424      * since it does not know about any shaping on the previous/following line
    425      * that might affect the cursor position. Callers must either avoid these
    426      * situations or handle the result specially.
    427      *
    428      * @param cursor the starting position of the cursor, between 0 and the
    429      * length of the line, inclusive
    430      * @param toLeft true if the caret is moving to the left.
    431      * @return the new offset.  If it is less than 0 or greater than the length
    432      * of the line, the previous/following line should be examined to get the
    433      * actual offset.
    434      */
    435     int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
    436         // 1) The caret marks the leading edge of a character. The character
    437         // logically before it might be on a different level, and the active caret
    438         // position is on the character at the lower level. If that character
    439         // was the previous character, the caret is on its trailing edge.
    440         // 2) Take this character/edge and move it in the indicated direction.
    441         // This gives you a new character and a new edge.
    442         // 3) This position is between two visually adjacent characters.  One of
    443         // these might be at a lower level.  The active position is on the
    444         // character at the lower level.
    445         // 4) If the active position is on the trailing edge of the character,
    446         // the new caret position is the following logical character, else it
    447         // is the character.
    448 
    449         int lineStart = 0;
    450         int lineEnd = mLen;
    451         boolean paraIsRtl = mDir == -1;
    452         int[] runs = mDirections.mDirections;
    453 
    454         int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
    455         boolean trailing = false;
    456 
    457         if (cursor == lineStart) {
    458             runIndex = -2;
    459         } else if (cursor == lineEnd) {
    460             runIndex = runs.length;
    461         } else {
    462           // First, get information about the run containing the character with
    463           // the active caret.
    464           for (runIndex = 0; runIndex < runs.length; runIndex += 2) {
    465             runStart = lineStart + runs[runIndex];
    466             if (cursor >= runStart) {
    467               runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK);
    468               if (runLimit > lineEnd) {
    469                   runLimit = lineEnd;
    470               }
    471               if (cursor < runLimit) {
    472                 runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
    473                     Layout.RUN_LEVEL_MASK;
    474                 if (cursor == runStart) {
    475                   // The caret is on a run boundary, see if we should
    476                   // use the position on the trailing edge of the previous
    477                   // logical character instead.
    478                   int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;
    479                   int pos = cursor - 1;
    480                   for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {
    481                     prevRunStart = lineStart + runs[prevRunIndex];
    482                     if (pos >= prevRunStart) {
    483                       prevRunLimit = prevRunStart +
    484                           (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK);
    485                       if (prevRunLimit > lineEnd) {
    486                           prevRunLimit = lineEnd;
    487                       }
    488                       if (pos < prevRunLimit) {
    489                         prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT)
    490                             & Layout.RUN_LEVEL_MASK;
    491                         if (prevRunLevel < runLevel) {
    492                           // Start from logically previous character.
    493                           runIndex = prevRunIndex;
    494                           runLevel = prevRunLevel;
    495                           runStart = prevRunStart;
    496                           runLimit = prevRunLimit;
    497                           trailing = true;
    498                           break;
    499                         }
    500                       }
    501                     }
    502                   }
    503                 }
    504                 break;
    505               }
    506             }
    507           }
    508 
    509           // caret might be == lineEnd.  This is generally a space or paragraph
    510           // separator and has an associated run, but might be the end of
    511           // text, in which case it doesn't.  If that happens, we ran off the
    512           // end of the run list, and runIndex == runs.length.  In this case,
    513           // we are at a run boundary so we skip the below test.
    514           if (runIndex != runs.length) {
    515               boolean runIsRtl = (runLevel & 0x1) != 0;
    516               boolean advance = toLeft == runIsRtl;
    517               if (cursor != (advance ? runLimit : runStart) || advance != trailing) {
    518                   // Moving within or into the run, so we can move logically.
    519                   newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit,
    520                           runIsRtl, cursor, advance);
    521                   // If the new position is internal to the run, we're at the strong
    522                   // position already so we're finished.
    523                   if (newCaret != (advance ? runLimit : runStart)) {
    524                       return newCaret;
    525                   }
    526               }
    527           }
    528         }
    529 
    530         // If newCaret is -1, we're starting at a run boundary and crossing
    531         // into another run. Otherwise we've arrived at a run boundary, and
    532         // need to figure out which character to attach to.  Note we might
    533         // need to run this twice, if we cross a run boundary and end up at
    534         // another run boundary.
    535         while (true) {
    536           boolean advance = toLeft == paraIsRtl;
    537           int otherRunIndex = runIndex + (advance ? 2 : -2);
    538           if (otherRunIndex >= 0 && otherRunIndex < runs.length) {
    539             int otherRunStart = lineStart + runs[otherRunIndex];
    540             int otherRunLimit = otherRunStart +
    541             (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK);
    542             if (otherRunLimit > lineEnd) {
    543                 otherRunLimit = lineEnd;
    544             }
    545             int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
    546                 Layout.RUN_LEVEL_MASK;
    547             boolean otherRunIsRtl = (otherRunLevel & 1) != 0;
    548 
    549             advance = toLeft == otherRunIsRtl;
    550             if (newCaret == -1) {
    551                 newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart,
    552                         otherRunLimit, otherRunIsRtl,
    553                         advance ? otherRunStart : otherRunLimit, advance);
    554                 if (newCaret == (advance ? otherRunLimit : otherRunStart)) {
    555                     // Crossed and ended up at a new boundary,
    556                     // repeat a second and final time.
    557                     runIndex = otherRunIndex;
    558                     runLevel = otherRunLevel;
    559                     continue;
    560                 }
    561                 break;
    562             }
    563 
    564             // The new caret is at a boundary.
    565             if (otherRunLevel < runLevel) {
    566               // The strong character is in the other run.
    567               newCaret = advance ? otherRunStart : otherRunLimit;
    568             }
    569             break;
    570           }
    571 
    572           if (newCaret == -1) {
    573               // We're walking off the end of the line.  The paragraph
    574               // level is always equal to or lower than any internal level, so
    575               // the boundaries get the strong caret.
    576               newCaret = advance ? mLen + 1 : -1;
    577               break;
    578           }
    579 
    580           // Else we've arrived at the end of the line.  That's a strong position.
    581           // We might have arrived here by crossing over a run with no internal
    582           // breaks and dropping out of the above loop before advancing one final
    583           // time, so reset the caret.
    584           // Note, we use '<=' below to handle a situation where the only run
    585           // on the line is a counter-directional run.  If we're not advancing,
    586           // we can end up at the 'lineEnd' position but the caret we want is at
    587           // the lineStart.
    588           if (newCaret <= lineEnd) {
    589               newCaret = advance ? lineEnd : lineStart;
    590           }
    591           break;
    592         }
    593 
    594         return newCaret;
    595     }
    596 
    597     /**
    598      * Returns the next valid offset within this directional run, skipping
    599      * conjuncts and zero-width characters.  This should not be called to walk
    600      * off the end of the line, since the returned values might not be valid
    601      * on neighboring lines.  If the returned offset is less than zero or
    602      * greater than the line length, the offset should be recomputed on the
    603      * preceding or following line, respectively.
    604      *
    605      * @param runIndex the run index
    606      * @param runStart the start of the run
    607      * @param runLimit the limit of the run
    608      * @param runIsRtl true if the run is right-to-left
    609      * @param offset the offset
    610      * @param after true if the new offset should logically follow the provided
    611      * offset
    612      * @return the new offset
    613      */
    614     private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit,
    615             boolean runIsRtl, int offset, boolean after) {
    616 
    617         if (runIndex < 0 || offset == (after ? mLen : 0)) {
    618             // Walking off end of line.  Since we don't know
    619             // what cursor positions are available on other lines, we can't
    620             // return accurate values.  These are a guess.
    621             if (after) {
    622                 return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;
    623             }
    624             return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;
    625         }
    626 
    627         TextPaint wp = mWorkPaint;
    628         wp.set(mPaint);
    629 
    630         int spanStart = runStart;
    631         int spanLimit;
    632         if (mSpanned == null) {
    633             spanLimit = runLimit;
    634         } else {
    635             int target = after ? offset + 1 : offset;
    636             int limit = mStart + runLimit;
    637             while (true) {
    638                 spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,
    639                         MetricAffectingSpan.class) - mStart;
    640                 if (spanLimit >= target) {
    641                     break;
    642                 }
    643                 spanStart = spanLimit;
    644             }
    645 
    646             MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,
    647                     mStart + spanLimit, MetricAffectingSpan.class);
    648             spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);
    649 
    650             if (spans.length > 0) {
    651                 ReplacementSpan replacement = null;
    652                 for (int j = 0; j < spans.length; j++) {
    653                     MetricAffectingSpan span = spans[j];
    654                     if (span instanceof ReplacementSpan) {
    655                         replacement = (ReplacementSpan)span;
    656                     } else {
    657                         span.updateMeasureState(wp);
    658                     }
    659                 }
    660 
    661                 if (replacement != null) {
    662                     // If we have a replacement span, we're moving either to
    663                     // the start or end of this span.
    664                     return after ? spanLimit : spanStart;
    665                 }
    666             }
    667         }
    668 
    669         int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
    670         int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;
    671         if (mCharsValid) {
    672             return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,
    673                     flags, offset, cursorOpt);
    674         } else {
    675             return wp.getTextRunCursor(mText, mStart + spanStart,
    676                     mStart + spanLimit, flags, mStart + offset, cursorOpt) - mStart;
    677         }
    678     }
    679 
    680     /**
    681      * @param wp
    682      */
    683     private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
    684         final int previousTop     = fmi.top;
    685         final int previousAscent  = fmi.ascent;
    686         final int previousDescent = fmi.descent;
    687         final int previousBottom  = fmi.bottom;
    688         final int previousLeading = fmi.leading;
    689 
    690         wp.getFontMetricsInt(fmi);
    691 
    692         updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
    693                 previousLeading);
    694     }
    695 
    696     static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
    697             int previousDescent, int previousBottom, int previousLeading) {
    698         fmi.top     = Math.min(fmi.top,     previousTop);
    699         fmi.ascent  = Math.min(fmi.ascent,  previousAscent);
    700         fmi.descent = Math.max(fmi.descent, previousDescent);
    701         fmi.bottom  = Math.max(fmi.bottom,  previousBottom);
    702         fmi.leading = Math.max(fmi.leading, previousLeading);
    703     }
    704 
    705     /**
    706      * Utility function for measuring and rendering text.  The text must
    707      * not include a tab or emoji.
    708      *
    709      * @param wp the working paint
    710      * @param start the start of the text
    711      * @param end the end of the text
    712      * @param runIsRtl true if the run is right-to-left
    713      * @param c the canvas, can be null if rendering is not needed
    714      * @param x the edge of the run closest to the leading margin
    715      * @param top the top of the line
    716      * @param y the baseline
    717      * @param bottom the bottom of the line
    718      * @param fmi receives metrics information, can be null
    719      * @param needWidth true if the width of the run is needed
    720      * @return the signed width of the run based on the run direction; only
    721      * valid if needWidth is true
    722      */
    723     private float handleText(TextPaint wp, int start, int end,
    724             int contextStart, int contextEnd, boolean runIsRtl,
    725             Canvas c, float x, int top, int y, int bottom,
    726             FontMetricsInt fmi, boolean needWidth) {
    727 
    728         // Get metrics first (even for empty strings or "0" width runs)
    729         if (fmi != null) {
    730             expandMetricsFromPaint(fmi, wp);
    731         }
    732 
    733         int runLen = end - start;
    734         // No need to do anything if the run width is "0"
    735         if (runLen == 0) {
    736             return 0f;
    737         }
    738 
    739         float ret = 0;
    740 
    741         int contextLen = contextEnd - contextStart;
    742         if (needWidth || (c != null && (wp.bgColor != 0 || wp.underlineColor != 0 || runIsRtl))) {
    743             int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
    744             if (mCharsValid) {
    745                 ret = wp.getTextRunAdvances(mChars, start, runLen,
    746                         contextStart, contextLen, flags, null, 0);
    747             } else {
    748                 int delta = mStart;
    749                 ret = wp.getTextRunAdvances(mText, delta + start,
    750                         delta + end, delta + contextStart, delta + contextEnd,
    751                         flags, null, 0);
    752             }
    753         }
    754 
    755         if (c != null) {
    756             if (runIsRtl) {
    757                 x -= ret;
    758             }
    759 
    760             if (wp.bgColor != 0) {
    761                 int previousColor = wp.getColor();
    762                 Paint.Style previousStyle = wp.getStyle();
    763 
    764                 wp.setColor(wp.bgColor);
    765                 wp.setStyle(Paint.Style.FILL);
    766                 c.drawRect(x, top, x + ret, bottom, wp);
    767 
    768                 wp.setStyle(previousStyle);
    769                 wp.setColor(previousColor);
    770             }
    771 
    772             if (wp.underlineColor != 0) {
    773                 // kStdUnderline_Offset = 1/9, defined in SkTextFormatParams.h
    774                 float underlineTop = y + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize();
    775 
    776                 int previousColor = wp.getColor();
    777                 Paint.Style previousStyle = wp.getStyle();
    778                 boolean previousAntiAlias = wp.isAntiAlias();
    779 
    780                 wp.setStyle(Paint.Style.FILL);
    781                 wp.setAntiAlias(true);
    782 
    783                 wp.setColor(wp.underlineColor);
    784                 c.drawRect(x, underlineTop, x + ret, underlineTop + wp.underlineThickness, wp);
    785 
    786                 wp.setStyle(previousStyle);
    787                 wp.setColor(previousColor);
    788                 wp.setAntiAlias(previousAntiAlias);
    789             }
    790 
    791             drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
    792                     x, y + wp.baselineShift);
    793         }
    794 
    795         return runIsRtl ? -ret : ret;
    796     }
    797 
    798     /**
    799      * Utility function for measuring and rendering a replacement.
    800      *
    801      *
    802      * @param replacement the replacement
    803      * @param wp the work paint
    804      * @param start the start of the run
    805      * @param limit the limit of the run
    806      * @param runIsRtl true if the run is right-to-left
    807      * @param c the canvas, can be null if not rendering
    808      * @param x the edge of the replacement closest to the leading margin
    809      * @param top the top of the line
    810      * @param y the baseline
    811      * @param bottom the bottom of the line
    812      * @param fmi receives metrics information, can be null
    813      * @param needWidth true if the width of the replacement is needed
    814      * @return the signed width of the run based on the run direction; only
    815      * valid if needWidth is true
    816      */
    817     private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
    818             int start, int limit, boolean runIsRtl, Canvas c,
    819             float x, int top, int y, int bottom, FontMetricsInt fmi,
    820             boolean needWidth) {
    821 
    822         float ret = 0;
    823 
    824         int textStart = mStart + start;
    825         int textLimit = mStart + limit;
    826 
    827         if (needWidth || (c != null && runIsRtl)) {
    828             int previousTop = 0;
    829             int previousAscent = 0;
    830             int previousDescent = 0;
    831             int previousBottom = 0;
    832             int previousLeading = 0;
    833 
    834             boolean needUpdateMetrics = (fmi != null);
    835 
    836             if (needUpdateMetrics) {
    837                 previousTop     = fmi.top;
    838                 previousAscent  = fmi.ascent;
    839                 previousDescent = fmi.descent;
    840                 previousBottom  = fmi.bottom;
    841                 previousLeading = fmi.leading;
    842             }
    843 
    844             ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
    845 
    846             if (needUpdateMetrics) {
    847                 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
    848                         previousLeading);
    849             }
    850         }
    851 
    852         if (c != null) {
    853             if (runIsRtl) {
    854                 x -= ret;
    855             }
    856             replacement.draw(c, mText, textStart, textLimit,
    857                     x, top, y, bottom, wp);
    858         }
    859 
    860         return runIsRtl ? -ret : ret;
    861     }
    862 
    863     private static class SpanSet<E> {
    864         int numberOfSpans;
    865         E[] spans;
    866         int[] spanStarts;
    867         int[] spanEnds;
    868         int[] spanFlags;
    869         final Class<? extends E> classType;
    870 
    871         SpanSet(Class<? extends E> type) {
    872             classType = type;
    873             numberOfSpans = 0;
    874         }
    875 
    876         @SuppressWarnings("unchecked")
    877         public void init(Spanned spanned, int start, int limit) {
    878             final E[] allSpans = spanned.getSpans(start, limit, classType);
    879             final int length = allSpans.length;
    880 
    881             if (length > 0 && (spans == null || spans.length < length)) {
    882                 // These arrays may end up being too large because of empty spans
    883                 spans = (E[]) Array.newInstance(classType, length);
    884                 spanStarts = new int[length];
    885                 spanEnds = new int[length];
    886                 spanFlags = new int[length];
    887             }
    888 
    889             numberOfSpans = 0;
    890             for (int i = 0; i < length; i++) {
    891                 final E span = allSpans[i];
    892 
    893                 final int spanStart = spanned.getSpanStart(span);
    894                 final int spanEnd = spanned.getSpanEnd(span);
    895                 if (spanStart == spanEnd) continue;
    896 
    897                 final int spanFlag = spanned.getSpanFlags(span);
    898 
    899                 spans[numberOfSpans] = span;
    900                 spanStarts[numberOfSpans] = spanStart;
    901                 spanEnds[numberOfSpans] = spanEnd;
    902                 spanFlags[numberOfSpans] = spanFlag;
    903 
    904                 numberOfSpans++;
    905             }
    906         }
    907 
    908         public boolean hasSpansIntersecting(int start, int end) {
    909             for (int i = 0; i < numberOfSpans; i++) {
    910                 // equal test is valid since both intervals are not empty by construction
    911                 if (spanStarts[i] >= end || spanEnds[i] <= start) continue;
    912                 return true;
    913             }
    914             return false;
    915         }
    916 
    917         int getNextTransition(int start, int limit) {
    918             for (int i = 0; i < numberOfSpans; i++) {
    919                 final int spanStart = spanStarts[i];
    920                 final int spanEnd = spanEnds[i];
    921                 if (spanStart > start && spanStart < limit) limit = spanStart;
    922                 if (spanEnd > start && spanEnd < limit) limit = spanEnd;
    923             }
    924             return limit;
    925         }
    926 
    927         public void recycle() {
    928             // The spans array is guaranteed to be not null when numberOfSpans is > 0
    929             for (int i = 0; i < numberOfSpans; i++) {
    930                 spans[i] = null; // prevent a leak: no reference kept when TextLine is recycled
    931             }
    932         }
    933     }
    934 
    935     /**
    936      * Utility function for handling a unidirectional run.  The run must not
    937      * contain tabs or emoji but can contain styles.
    938      *
    939      *
    940      * @param start the line-relative start of the run
    941      * @param measureLimit the offset to measure to, between start and limit inclusive
    942      * @param limit the limit of the run
    943      * @param runIsRtl true if the run is right-to-left
    944      * @param c the canvas, can be null
    945      * @param x the end of the run closest to the leading margin
    946      * @param top the top of the line
    947      * @param y the baseline
    948      * @param bottom the bottom of the line
    949      * @param fmi receives metrics information, can be null
    950      * @param needWidth true if the width is required
    951      * @return the signed width of the run based on the run direction; only
    952      * valid if needWidth is true
    953      */
    954     private float handleRun(int start, int measureLimit,
    955             int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
    956             int bottom, FontMetricsInt fmi, boolean needWidth) {
    957 
    958         // Case of an empty line, make sure we update fmi according to mPaint
    959         if (start == measureLimit) {
    960             TextPaint wp = mWorkPaint;
    961             wp.set(mPaint);
    962             if (fmi != null) {
    963                 expandMetricsFromPaint(fmi, wp);
    964             }
    965             return 0f;
    966         }
    967 
    968         if (mSpanned == null) {
    969             TextPaint wp = mWorkPaint;
    970             wp.set(mPaint);
    971             final int mlimit = measureLimit;
    972             return handleText(wp, start, mlimit, start, limit, runIsRtl, c, x, top,
    973                     y, bottom, fmi, needWidth || mlimit < measureLimit);
    974         }
    975 
    976         mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
    977         mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
    978 
    979         // Shaping needs to take into account context up to metric boundaries,
    980         // but rendering needs to take into account character style boundaries.
    981         // So we iterate through metric runs to get metric bounds,
    982         // then within each metric run iterate through character style runs
    983         // for the run bounds.
    984         final float originalX = x;
    985         for (int i = start, inext; i < measureLimit; i = inext) {
    986             TextPaint wp = mWorkPaint;
    987             wp.set(mPaint);
    988 
    989             inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
    990                     mStart;
    991             int mlimit = Math.min(inext, measureLimit);
    992 
    993             ReplacementSpan replacement = null;
    994 
    995             for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
    996                 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
    997                 // empty by construction. This special case in getSpans() explains the >= & <= tests
    998                 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||
    999                         (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
   1000                 MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
   1001                 if (span instanceof ReplacementSpan) {
   1002                     replacement = (ReplacementSpan)span;
   1003                 } else {
   1004                     // We might have a replacement that uses the draw
   1005                     // state, otherwise measure state would suffice.
   1006                     span.updateDrawState(wp);
   1007                 }
   1008             }
   1009 
   1010             if (replacement != null) {
   1011                 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
   1012                         bottom, fmi, needWidth || mlimit < measureLimit);
   1013                 continue;
   1014             }
   1015 
   1016             if (c == null) {
   1017                 x += handleText(wp, i, mlimit, i, inext, runIsRtl, c, x, top,
   1018                         y, bottom, fmi, needWidth || mlimit < measureLimit);
   1019             } else {
   1020                 for (int j = i, jnext; j < mlimit; j = jnext) {
   1021                     jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + mlimit) -
   1022                             mStart;
   1023 
   1024                     wp.set(mPaint);
   1025                     for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
   1026                         // Intentionally using >= and <= as explained above
   1027                         if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + jnext) ||
   1028                                 (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
   1029 
   1030                         CharacterStyle span = mCharacterStyleSpanSet.spans[k];
   1031                         span.updateDrawState(wp);
   1032                     }
   1033 
   1034                     x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,
   1035                             top, y, bottom, fmi, needWidth || jnext < measureLimit);
   1036                 }
   1037             }
   1038         }
   1039 
   1040         return x - originalX;
   1041     }
   1042 
   1043     /**
   1044      * Render a text run with the set-up paint.
   1045      *
   1046      * @param c the canvas
   1047      * @param wp the paint used to render the text
   1048      * @param start the start of the run
   1049      * @param end the end of the run
   1050      * @param contextStart the start of context for the run
   1051      * @param contextEnd the end of the context for the run
   1052      * @param runIsRtl true if the run is right-to-left
   1053      * @param x the x position of the left edge of the run
   1054      * @param y the baseline of the run
   1055      */
   1056     private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
   1057             int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
   1058 
   1059         int flags = runIsRtl ? Canvas.DIRECTION_RTL : Canvas.DIRECTION_LTR;
   1060         if (mCharsValid) {
   1061             int count = end - start;
   1062             int contextCount = contextEnd - contextStart;
   1063             c.drawTextRun(mChars, start, count, contextStart, contextCount,
   1064                     x, y, flags, wp);
   1065         } else {
   1066             int delta = mStart;
   1067             c.drawTextRun(mText, delta + start, delta + end,
   1068                     delta + contextStart, delta + contextEnd, x, y, flags, wp);
   1069         }
   1070     }
   1071 
   1072     /**
   1073      * Returns the ascent of the text at start.  This is used for scaling
   1074      * emoji.
   1075      *
   1076      * @param pos the line-relative position
   1077      * @return the ascent of the text at start
   1078      */
   1079     float ascent(int pos) {
   1080         if (mSpanned == null) {
   1081             return mPaint.ascent();
   1082         }
   1083 
   1084         pos += mStart;
   1085         MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1, MetricAffectingSpan.class);
   1086         if (spans.length == 0) {
   1087             return mPaint.ascent();
   1088         }
   1089 
   1090         TextPaint wp = mWorkPaint;
   1091         wp.set(mPaint);
   1092         for (MetricAffectingSpan span : spans) {
   1093             span.updateMeasureState(wp);
   1094         }
   1095         return wp.ascent();
   1096     }
   1097 
   1098     /**
   1099      * Returns the next tab position.
   1100      *
   1101      * @param h the (unsigned) offset from the leading margin
   1102      * @return the (unsigned) tab position after this offset
   1103      */
   1104     float nextTab(float h) {
   1105         if (mTabs != null) {
   1106             return mTabs.nextTab(h);
   1107         }
   1108         return TabStops.nextDefaultStop(h, TAB_INCREMENT);
   1109     }
   1110 
   1111     private static final int TAB_INCREMENT = 20;
   1112 }
   1113