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