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.annotation.NonNull;
     20 import android.annotation.Nullable;
     21 import android.graphics.Canvas;
     22 import android.graphics.Paint;
     23 import android.graphics.Paint.FontMetricsInt;
     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.util.ArrayList;
     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 
     62     // Additional width of whitespace for justification. This value is per whitespace, thus
     63     // the line width will increase by mAddedWidth x (number of stretchable whitespaces).
     64     private float mAddedWidth;
     65 
     66     private final TextPaint mWorkPaint = new TextPaint();
     67     private final TextPaint mActivePaint = new TextPaint();
     68     private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =
     69             new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);
     70     private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =
     71             new SpanSet<CharacterStyle>(CharacterStyle.class);
     72     private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =
     73             new SpanSet<ReplacementSpan>(ReplacementSpan.class);
     74 
     75     private final DecorationInfo mDecorationInfo = new DecorationInfo();
     76     private final ArrayList<DecorationInfo> mDecorations = new ArrayList();
     77 
     78     private static final TextLine[] sCached = new TextLine[3];
     79 
     80     /**
     81      * Returns a new TextLine from the shared pool.
     82      *
     83      * @return an uninitialized TextLine
     84      */
     85     static TextLine obtain() {
     86         TextLine tl;
     87         synchronized (sCached) {
     88             for (int i = sCached.length; --i >= 0;) {
     89                 if (sCached[i] != null) {
     90                     tl = sCached[i];
     91                     sCached[i] = null;
     92                     return tl;
     93                 }
     94             }
     95         }
     96         tl = new TextLine();
     97         if (DEBUG) {
     98             Log.v("TLINE", "new: " + tl);
     99         }
    100         return tl;
    101     }
    102 
    103     /**
    104      * Puts a TextLine back into the shared pool. Do not use this TextLine once
    105      * it has been returned.
    106      * @param tl the textLine
    107      * @return null, as a convenience from clearing references to the provided
    108      * TextLine
    109      */
    110     static TextLine recycle(TextLine tl) {
    111         tl.mText = null;
    112         tl.mPaint = null;
    113         tl.mDirections = null;
    114         tl.mSpanned = null;
    115         tl.mTabs = null;
    116         tl.mChars = null;
    117 
    118         tl.mMetricAffectingSpanSpanSet.recycle();
    119         tl.mCharacterStyleSpanSet.recycle();
    120         tl.mReplacementSpanSpanSet.recycle();
    121 
    122         synchronized(sCached) {
    123             for (int i = 0; i < sCached.length; ++i) {
    124                 if (sCached[i] == null) {
    125                     sCached[i] = tl;
    126                     break;
    127                 }
    128             }
    129         }
    130         return null;
    131     }
    132 
    133     /**
    134      * Initializes a TextLine and prepares it for use.
    135      *
    136      * @param paint the base paint for the line
    137      * @param text the text, can be Styled
    138      * @param start the start of the line relative to the text
    139      * @param limit the limit of the line relative to the text
    140      * @param dir the paragraph direction of this line
    141      * @param directions the directions information of this line
    142      * @param hasTabs true if the line might contain tabs
    143      * @param tabStops the tabStops. Can be null.
    144      */
    145     void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
    146             Directions directions, boolean hasTabs, TabStops tabStops) {
    147         mPaint = paint;
    148         mText = text;
    149         mStart = start;
    150         mLen = limit - start;
    151         mDir = dir;
    152         mDirections = directions;
    153         if (mDirections == null) {
    154             throw new IllegalArgumentException("Directions cannot be null");
    155         }
    156         mHasTabs = hasTabs;
    157         mSpanned = null;
    158 
    159         boolean hasReplacement = false;
    160         if (text instanceof Spanned) {
    161             mSpanned = (Spanned) text;
    162             mReplacementSpanSpanSet.init(mSpanned, start, limit);
    163             hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
    164         }
    165 
    166         mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT;
    167 
    168         if (mCharsValid) {
    169             if (mChars == null || mChars.length < mLen) {
    170                 mChars = ArrayUtils.newUnpaddedCharArray(mLen);
    171             }
    172             TextUtils.getChars(text, start, limit, mChars, 0);
    173             if (hasReplacement) {
    174                 // Handle these all at once so we don't have to do it as we go.
    175                 // Replace the first character of each replacement run with the
    176                 // object-replacement character and the remainder with zero width
    177                 // non-break space aka BOM.  Cursor movement code skips these
    178                 // zero-width characters.
    179                 char[] chars = mChars;
    180                 for (int i = start, inext; i < limit; i = inext) {
    181                     inext = mReplacementSpanSpanSet.getNextTransition(i, limit);
    182                     if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)) {
    183                         // transition into a span
    184                         chars[i - start] = '\ufffc';
    185                         for (int j = i - start + 1, e = inext - start; j < e; ++j) {
    186                             chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
    187                         }
    188                     }
    189                 }
    190             }
    191         }
    192         mTabs = tabStops;
    193         mAddedWidth = 0;
    194     }
    195 
    196     /**
    197      * Justify the line to the given width.
    198      */
    199     void justify(float justifyWidth) {
    200         int end = mLen;
    201         while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) {
    202             end--;
    203         }
    204         final int spaces = countStretchableSpaces(0, end);
    205         if (spaces == 0) {
    206             // There are no stretchable spaces, so we can't help the justification by adding any
    207             // width.
    208             return;
    209         }
    210         final float width = Math.abs(measure(end, false, null));
    211         mAddedWidth = (justifyWidth - width) / spaces;
    212     }
    213 
    214     /**
    215      * Renders the TextLine.
    216      *
    217      * @param c the canvas to render on
    218      * @param x the leading margin position
    219      * @param top the top of the line
    220      * @param y the baseline
    221      * @param bottom the bottom of the line
    222      */
    223     void draw(Canvas c, float x, int top, int y, int bottom) {
    224         if (!mHasTabs) {
    225             if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
    226                 drawRun(c, 0, mLen, false, x, top, y, bottom, false);
    227                 return;
    228             }
    229             if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
    230                 drawRun(c, 0, mLen, true, x, top, y, bottom, false);
    231                 return;
    232             }
    233         }
    234 
    235         float h = 0;
    236         int[] runs = mDirections.mDirections;
    237 
    238         int lastRunIndex = runs.length - 2;
    239         for (int i = 0; i < runs.length; i += 2) {
    240             int runStart = runs[i];
    241             int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
    242             if (runLimit > mLen) {
    243                 runLimit = mLen;
    244             }
    245             boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
    246 
    247             int segstart = runStart;
    248             for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
    249                 int codept = 0;
    250                 if (mHasTabs && j < runLimit) {
    251                     codept = mChars[j];
    252                     if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
    253                         codept = Character.codePointAt(mChars, j);
    254                         if (codept > 0xFFFF) {
    255                             ++j;
    256                             continue;
    257                         }
    258                     }
    259                 }
    260 
    261                 if (j == runLimit || codept == '\t') {
    262                     h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom,
    263                             i != lastRunIndex || j != mLen);
    264 
    265                     if (codept == '\t') {
    266                         h = mDir * nextTab(h * mDir);
    267                     }
    268                     segstart = j + 1;
    269                 }
    270             }
    271         }
    272     }
    273 
    274     /**
    275      * Returns metrics information for the entire line.
    276      *
    277      * @param fmi receives font metrics information, can be null
    278      * @return the signed width of the line
    279      */
    280     float metrics(FontMetricsInt fmi) {
    281         return measure(mLen, false, fmi);
    282     }
    283 
    284     /**
    285      * Returns information about a position on the line.
    286      *
    287      * @param offset the line-relative character offset, between 0 and the
    288      * line length, inclusive
    289      * @param trailing true to measure the trailing edge of the character
    290      * before offset, false to measure the leading edge of the character
    291      * at offset.
    292      * @param fmi receives metrics information about the requested
    293      * character, can be null.
    294      * @return the signed offset from the leading margin to the requested
    295      * character edge.
    296      */
    297     float measure(int offset, boolean trailing, FontMetricsInt fmi) {
    298         int target = trailing ? offset - 1 : offset;
    299         if (target < 0) {
    300             return 0;
    301         }
    302 
    303         float h = 0;
    304 
    305         if (!mHasTabs) {
    306             if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
    307                 return measureRun(0, offset, mLen, false, fmi);
    308             }
    309             if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
    310                 return measureRun(0, offset, mLen, true, fmi);
    311             }
    312         }
    313 
    314         char[] chars = mChars;
    315         int[] runs = mDirections.mDirections;
    316         for (int i = 0; i < runs.length; i += 2) {
    317             int runStart = runs[i];
    318             int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
    319             if (runLimit > mLen) {
    320                 runLimit = mLen;
    321             }
    322             boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
    323 
    324             int segstart = runStart;
    325             for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
    326                 int codept = 0;
    327                 if (mHasTabs && j < runLimit) {
    328                     codept = chars[j];
    329                     if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
    330                         codept = Character.codePointAt(chars, j);
    331                         if (codept > 0xFFFF) {
    332                             ++j;
    333                             continue;
    334                         }
    335                     }
    336                 }
    337 
    338                 if (j == runLimit || codept == '\t') {
    339                     boolean inSegment = target >= segstart && target < j;
    340 
    341                     boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
    342                     if (inSegment && advance) {
    343                         return h += measureRun(segstart, offset, j, runIsRtl, fmi);
    344                     }
    345 
    346                     float w = measureRun(segstart, j, j, runIsRtl, fmi);
    347                     h += advance ? w : -w;
    348 
    349                     if (inSegment) {
    350                         return h += measureRun(segstart, offset, j, runIsRtl, null);
    351                     }
    352 
    353                     if (codept == '\t') {
    354                         if (offset == j) {
    355                             return h;
    356                         }
    357                         h = mDir * nextTab(h * mDir);
    358                         if (target == j) {
    359                             return h;
    360                         }
    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         wp.setWordSpacing(mAddedWidth);
    631 
    632         int spanStart = runStart;
    633         int spanLimit;
    634         if (mSpanned == null) {
    635             spanLimit = runLimit;
    636         } else {
    637             int target = after ? offset + 1 : offset;
    638             int limit = mStart + runLimit;
    639             while (true) {
    640                 spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,
    641                         MetricAffectingSpan.class) - mStart;
    642                 if (spanLimit >= target) {
    643                     break;
    644                 }
    645                 spanStart = spanLimit;
    646             }
    647 
    648             MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,
    649                     mStart + spanLimit, MetricAffectingSpan.class);
    650             spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);
    651 
    652             if (spans.length > 0) {
    653                 ReplacementSpan replacement = null;
    654                 for (int j = 0; j < spans.length; j++) {
    655                     MetricAffectingSpan span = spans[j];
    656                     if (span instanceof ReplacementSpan) {
    657                         replacement = (ReplacementSpan)span;
    658                     } else {
    659                         span.updateMeasureState(wp);
    660                     }
    661                 }
    662 
    663                 if (replacement != null) {
    664                     // If we have a replacement span, we're moving either to
    665                     // the start or end of this span.
    666                     return after ? spanLimit : spanStart;
    667                 }
    668             }
    669         }
    670 
    671         int dir = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
    672         int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;
    673         if (mCharsValid) {
    674             return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,
    675                     dir, offset, cursorOpt);
    676         } else {
    677             return wp.getTextRunCursor(mText, mStart + spanStart,
    678                     mStart + spanLimit, dir, mStart + offset, cursorOpt) - mStart;
    679         }
    680     }
    681 
    682     /**
    683      * @param wp
    684      */
    685     private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
    686         final int previousTop     = fmi.top;
    687         final int previousAscent  = fmi.ascent;
    688         final int previousDescent = fmi.descent;
    689         final int previousBottom  = fmi.bottom;
    690         final int previousLeading = fmi.leading;
    691 
    692         wp.getFontMetricsInt(fmi);
    693 
    694         updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
    695                 previousLeading);
    696     }
    697 
    698     static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
    699             int previousDescent, int previousBottom, int previousLeading) {
    700         fmi.top     = Math.min(fmi.top,     previousTop);
    701         fmi.ascent  = Math.min(fmi.ascent,  previousAscent);
    702         fmi.descent = Math.max(fmi.descent, previousDescent);
    703         fmi.bottom  = Math.max(fmi.bottom,  previousBottom);
    704         fmi.leading = Math.max(fmi.leading, previousLeading);
    705     }
    706 
    707     private static void drawStroke(TextPaint wp, Canvas c, int color, float position,
    708             float thickness, float xleft, float xright, float baseline) {
    709         final float strokeTop = baseline + wp.baselineShift + position;
    710 
    711         final int previousColor = wp.getColor();
    712         final Paint.Style previousStyle = wp.getStyle();
    713         final boolean previousAntiAlias = wp.isAntiAlias();
    714 
    715         wp.setStyle(Paint.Style.FILL);
    716         wp.setAntiAlias(true);
    717 
    718         wp.setColor(color);
    719         c.drawRect(xleft, strokeTop, xright, strokeTop + thickness, wp);
    720 
    721         wp.setStyle(previousStyle);
    722         wp.setColor(previousColor);
    723         wp.setAntiAlias(previousAntiAlias);
    724     }
    725 
    726     private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
    727             boolean runIsRtl, int offset) {
    728         if (mCharsValid) {
    729             return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset);
    730         } else {
    731             final int delta = mStart;
    732             return wp.getRunAdvance(mText, delta + start, delta + end,
    733                     delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
    734         }
    735     }
    736 
    737     /**
    738      * Utility function for measuring and rendering text.  The text must
    739      * not include a tab.
    740      *
    741      * @param wp the working paint
    742      * @param start the start of the text
    743      * @param end the end of the text
    744      * @param runIsRtl true if the run is right-to-left
    745      * @param c the canvas, can be null if rendering is not needed
    746      * @param x the edge of the run closest to the leading margin
    747      * @param top the top of the line
    748      * @param y the baseline
    749      * @param bottom the bottom of the line
    750      * @param fmi receives metrics information, can be null
    751      * @param needWidth true if the width of the run is needed
    752      * @param offset the offset for the purpose of measuring
    753      * @param decorations the list of locations and paremeters for drawing decorations
    754      * @return the signed width of the run based on the run direction; only
    755      * valid if needWidth is true
    756      */
    757     private float handleText(TextPaint wp, int start, int end,
    758             int contextStart, int contextEnd, boolean runIsRtl,
    759             Canvas c, float x, int top, int y, int bottom,
    760             FontMetricsInt fmi, boolean needWidth, int offset,
    761             @Nullable ArrayList<DecorationInfo> decorations) {
    762 
    763         wp.setWordSpacing(mAddedWidth);
    764         // Get metrics first (even for empty strings or "0" width runs)
    765         if (fmi != null) {
    766             expandMetricsFromPaint(fmi, wp);
    767         }
    768 
    769         // No need to do anything if the run width is "0"
    770         if (end == start) {
    771             return 0f;
    772         }
    773 
    774         float totalWidth = 0;
    775 
    776         final int numDecorations = decorations == null ? 0 : decorations.size();
    777         if (needWidth || (c != null && (wp.bgColor != 0 || numDecorations != 0 || runIsRtl))) {
    778             totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset);
    779         }
    780 
    781         if (c != null) {
    782             final float leftX, rightX;
    783             if (runIsRtl) {
    784                 leftX = x - totalWidth;
    785                 rightX = x;
    786             } else {
    787                 leftX = x;
    788                 rightX = x + totalWidth;
    789             }
    790 
    791             if (wp.bgColor != 0) {
    792                 int previousColor = wp.getColor();
    793                 Paint.Style previousStyle = wp.getStyle();
    794 
    795                 wp.setColor(wp.bgColor);
    796                 wp.setStyle(Paint.Style.FILL);
    797                 c.drawRect(leftX, top, rightX, bottom, wp);
    798 
    799                 wp.setStyle(previousStyle);
    800                 wp.setColor(previousColor);
    801             }
    802 
    803             if (numDecorations != 0) {
    804                 for (int i = 0; i < numDecorations; i++) {
    805                     final DecorationInfo info = decorations.get(i);
    806 
    807                     final int decorationStart = Math.max(info.start, start);
    808                     final int decorationEnd = Math.min(info.end, offset);
    809                     float decorationStartAdvance = getRunAdvance(
    810                             wp, start, end, contextStart, contextEnd, runIsRtl, decorationStart);
    811                     float decorationEndAdvance = getRunAdvance(
    812                             wp, start, end, contextStart, contextEnd, runIsRtl, decorationEnd);
    813                     final float decorationXLeft, decorationXRight;
    814                     if (runIsRtl) {
    815                         decorationXLeft = rightX - decorationEndAdvance;
    816                         decorationXRight = rightX - decorationStartAdvance;
    817                     } else {
    818                         decorationXLeft = leftX + decorationStartAdvance;
    819                         decorationXRight = leftX + decorationEndAdvance;
    820                     }
    821 
    822                     // Theoretically, there could be cases where both Paint's and TextPaint's
    823                     // setUnderLineText() are called. For backward compatibility, we need to draw
    824                     // both underlines, the one with custom color first.
    825                     if (info.underlineColor != 0) {
    826                         drawStroke(wp, c, info.underlineColor, wp.getUnderlinePosition(),
    827                                 info.underlineThickness, decorationXLeft, decorationXRight, y);
    828                     }
    829                     if (info.isUnderlineText) {
    830                         final float thickness =
    831                                 Math.max(((Paint) wp).getUnderlineThickness(), 1.0f);
    832                         drawStroke(wp, c, wp.getColor(), wp.getUnderlinePosition(), thickness,
    833                                 decorationXLeft, decorationXRight, y);
    834                     }
    835 
    836                     if (info.isStrikeThruText) {
    837                         final float thickness =
    838                                 Math.max(((Paint) wp).getStrikeThruThickness(), 1.0f);
    839                         drawStroke(wp, c, wp.getColor(), wp.getStrikeThruPosition(), thickness,
    840                                 decorationXLeft, decorationXRight, y);
    841                     }
    842                 }
    843             }
    844 
    845             drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
    846                     leftX, y + wp.baselineShift);
    847         }
    848 
    849         return runIsRtl ? -totalWidth : totalWidth;
    850     }
    851 
    852     /**
    853      * Utility function for measuring and rendering a replacement.
    854      *
    855      *
    856      * @param replacement the replacement
    857      * @param wp the work paint
    858      * @param start the start of the run
    859      * @param limit the limit of the run
    860      * @param runIsRtl true if the run is right-to-left
    861      * @param c the canvas, can be null if not rendering
    862      * @param x the edge of the replacement closest to the leading margin
    863      * @param top the top of the line
    864      * @param y the baseline
    865      * @param bottom the bottom of the line
    866      * @param fmi receives metrics information, can be null
    867      * @param needWidth true if the width of the replacement is needed
    868      * @return the signed width of the run based on the run direction; only
    869      * valid if needWidth is true
    870      */
    871     private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
    872             int start, int limit, boolean runIsRtl, Canvas c,
    873             float x, int top, int y, int bottom, FontMetricsInt fmi,
    874             boolean needWidth) {
    875 
    876         float ret = 0;
    877 
    878         int textStart = mStart + start;
    879         int textLimit = mStart + limit;
    880 
    881         if (needWidth || (c != null && runIsRtl)) {
    882             int previousTop = 0;
    883             int previousAscent = 0;
    884             int previousDescent = 0;
    885             int previousBottom = 0;
    886             int previousLeading = 0;
    887 
    888             boolean needUpdateMetrics = (fmi != null);
    889 
    890             if (needUpdateMetrics) {
    891                 previousTop     = fmi.top;
    892                 previousAscent  = fmi.ascent;
    893                 previousDescent = fmi.descent;
    894                 previousBottom  = fmi.bottom;
    895                 previousLeading = fmi.leading;
    896             }
    897 
    898             ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
    899 
    900             if (needUpdateMetrics) {
    901                 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
    902                         previousLeading);
    903             }
    904         }
    905 
    906         if (c != null) {
    907             if (runIsRtl) {
    908                 x -= ret;
    909             }
    910             replacement.draw(c, mText, textStart, textLimit,
    911                     x, top, y, bottom, wp);
    912         }
    913 
    914         return runIsRtl ? -ret : ret;
    915     }
    916 
    917     private int adjustHyphenEdit(int start, int limit, int hyphenEdit) {
    918         int result = hyphenEdit;
    919         // Only draw hyphens on first or last run in line. Disable them otherwise.
    920         if (start > 0) { // not the first run
    921             result &= ~Paint.HYPHENEDIT_MASK_START_OF_LINE;
    922         }
    923         if (limit < mLen) { // not the last run
    924             result &= ~Paint.HYPHENEDIT_MASK_END_OF_LINE;
    925         }
    926         return result;
    927     }
    928 
    929     private static final class DecorationInfo {
    930         public boolean isStrikeThruText;
    931         public boolean isUnderlineText;
    932         public int underlineColor;
    933         public float underlineThickness;
    934         public int start = -1;
    935         public int end = -1;
    936 
    937         public boolean hasDecoration() {
    938             return isStrikeThruText || isUnderlineText || underlineColor != 0;
    939         }
    940 
    941         // Copies the info, but not the start and end range.
    942         public DecorationInfo copyInfo() {
    943             final DecorationInfo copy = new DecorationInfo();
    944             copy.isStrikeThruText = isStrikeThruText;
    945             copy.isUnderlineText = isUnderlineText;
    946             copy.underlineColor = underlineColor;
    947             copy.underlineThickness = underlineThickness;
    948             return copy;
    949         }
    950     }
    951 
    952     private void extractDecorationInfo(@NonNull TextPaint paint, @NonNull DecorationInfo info) {
    953         info.isStrikeThruText = paint.isStrikeThruText();
    954         if (info.isStrikeThruText) {
    955             paint.setStrikeThruText(false);
    956         }
    957         info.isUnderlineText = paint.isUnderlineText();
    958         if (info.isUnderlineText) {
    959             paint.setUnderlineText(false);
    960         }
    961         info.underlineColor = paint.underlineColor;
    962         info.underlineThickness = paint.underlineThickness;
    963         paint.setUnderlineText(0, 0.0f);
    964     }
    965 
    966     /**
    967      * Utility function for handling a unidirectional run.  The run must not
    968      * contain tabs but can contain styles.
    969      *
    970      *
    971      * @param start the line-relative start of the run
    972      * @param measureLimit the offset to measure to, between start and limit inclusive
    973      * @param limit the limit of the run
    974      * @param runIsRtl true if the run is right-to-left
    975      * @param c the canvas, can be null
    976      * @param x the end of the run closest to the leading margin
    977      * @param top the top of the line
    978      * @param y the baseline
    979      * @param bottom the bottom of the line
    980      * @param fmi receives metrics information, can be null
    981      * @param needWidth true if the width is required
    982      * @return the signed width of the run based on the run direction; only
    983      * valid if needWidth is true
    984      */
    985     private float handleRun(int start, int measureLimit,
    986             int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
    987             int bottom, FontMetricsInt fmi, boolean needWidth) {
    988 
    989         if (measureLimit < start || measureLimit > limit) {
    990             throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
    991                     + "start (" + start + ") and limit (" + limit + ") bounds");
    992         }
    993 
    994         // Case of an empty line, make sure we update fmi according to mPaint
    995         if (start == measureLimit) {
    996             final TextPaint wp = mWorkPaint;
    997             wp.set(mPaint);
    998             if (fmi != null) {
    999                 expandMetricsFromPaint(fmi, wp);
   1000             }
   1001             return 0f;
   1002         }
   1003 
   1004         final boolean needsSpanMeasurement;
   1005         if (mSpanned == null) {
   1006             needsSpanMeasurement = false;
   1007         } else {
   1008             mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
   1009             mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
   1010             needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0
   1011                     || mCharacterStyleSpanSet.numberOfSpans != 0;
   1012         }
   1013 
   1014         if (!needsSpanMeasurement) {
   1015             final TextPaint wp = mWorkPaint;
   1016             wp.set(mPaint);
   1017             wp.setHyphenEdit(adjustHyphenEdit(start, limit, wp.getHyphenEdit()));
   1018             return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,
   1019                     y, bottom, fmi, needWidth, measureLimit, null);
   1020         }
   1021 
   1022         // Shaping needs to take into account context up to metric boundaries,
   1023         // but rendering needs to take into account character style boundaries.
   1024         // So we iterate through metric runs to get metric bounds,
   1025         // then within each metric run iterate through character style runs
   1026         // for the run bounds.
   1027         final float originalX = x;
   1028         for (int i = start, inext; i < measureLimit; i = inext) {
   1029             final TextPaint wp = mWorkPaint;
   1030             wp.set(mPaint);
   1031 
   1032             inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
   1033                     mStart;
   1034             int mlimit = Math.min(inext, measureLimit);
   1035 
   1036             ReplacementSpan replacement = null;
   1037 
   1038             for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
   1039                 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
   1040                 // empty by construction. This special case in getSpans() explains the >= & <= tests
   1041                 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||
   1042                         (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
   1043                 final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
   1044                 if (span instanceof ReplacementSpan) {
   1045                     replacement = (ReplacementSpan)span;
   1046                 } else {
   1047                     // We might have a replacement that uses the draw
   1048                     // state, otherwise measure state would suffice.
   1049                     span.updateDrawState(wp);
   1050                 }
   1051             }
   1052 
   1053             if (replacement != null) {
   1054                 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
   1055                         bottom, fmi, needWidth || mlimit < measureLimit);
   1056                 continue;
   1057             }
   1058 
   1059             final TextPaint activePaint = mActivePaint;
   1060             activePaint.set(mPaint);
   1061             int activeStart = i;
   1062             int activeEnd = mlimit;
   1063             final DecorationInfo decorationInfo = mDecorationInfo;
   1064             mDecorations.clear();
   1065             for (int j = i, jnext; j < mlimit; j = jnext) {
   1066                 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
   1067                         mStart;
   1068 
   1069                 final int offset = Math.min(jnext, mlimit);
   1070                 wp.set(mPaint);
   1071                 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
   1072                     // Intentionally using >= and <= as explained above
   1073                     if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
   1074                             (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
   1075 
   1076                     final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
   1077                     span.updateDrawState(wp);
   1078                 }
   1079 
   1080                 extractDecorationInfo(wp, decorationInfo);
   1081 
   1082                 if (j == i) {
   1083                     // First chunk of text. We can't handle it yet, since we may need to merge it
   1084                     // with the next chunk. So we just save the TextPaint for future comparisons
   1085                     // and use.
   1086                     activePaint.set(wp);
   1087                 } else if (!wp.hasEqualAttributes(activePaint)) {
   1088                     // The style of the present chunk of text is substantially different from the
   1089                     // style of the previous chunk. We need to handle the active piece of text
   1090                     // and restart with the present chunk.
   1091                     activePaint.setHyphenEdit(adjustHyphenEdit(
   1092                             activeStart, activeEnd, mPaint.getHyphenEdit()));
   1093                     x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
   1094                             top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
   1095                             Math.min(activeEnd, mlimit), mDecorations);
   1096 
   1097                     activeStart = j;
   1098                     activePaint.set(wp);
   1099                     mDecorations.clear();
   1100                 } else {
   1101                     // The present TextPaint is substantially equal to the last TextPaint except
   1102                     // perhaps for decorations. We just need to expand the active piece of text to
   1103                     // include the present chunk, which we always do anyway. We don't need to save
   1104                     // wp to activePaint, since they are already equal.
   1105                 }
   1106 
   1107                 activeEnd = jnext;
   1108                 if (decorationInfo.hasDecoration()) {
   1109                     final DecorationInfo copy = decorationInfo.copyInfo();
   1110                     copy.start = j;
   1111                     copy.end = jnext;
   1112                     mDecorations.add(copy);
   1113                 }
   1114             }
   1115             // Handle the final piece of text.
   1116             activePaint.setHyphenEdit(adjustHyphenEdit(
   1117                     activeStart, activeEnd, mPaint.getHyphenEdit()));
   1118             x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
   1119                     top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
   1120                     Math.min(activeEnd, mlimit), mDecorations);
   1121         }
   1122 
   1123         return x - originalX;
   1124     }
   1125 
   1126     /**
   1127      * Render a text run with the set-up paint.
   1128      *
   1129      * @param c the canvas
   1130      * @param wp the paint used to render the text
   1131      * @param start the start of the run
   1132      * @param end the end of the run
   1133      * @param contextStart the start of context for the run
   1134      * @param contextEnd the end of the context for the run
   1135      * @param runIsRtl true if the run is right-to-left
   1136      * @param x the x position of the left edge of the run
   1137      * @param y the baseline of the run
   1138      */
   1139     private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
   1140             int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
   1141 
   1142         if (mCharsValid) {
   1143             int count = end - start;
   1144             int contextCount = contextEnd - contextStart;
   1145             c.drawTextRun(mChars, start, count, contextStart, contextCount,
   1146                     x, y, runIsRtl, wp);
   1147         } else {
   1148             int delta = mStart;
   1149             c.drawTextRun(mText, delta + start, delta + end,
   1150                     delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
   1151         }
   1152     }
   1153 
   1154     /**
   1155      * Returns the next tab position.
   1156      *
   1157      * @param h the (unsigned) offset from the leading margin
   1158      * @return the (unsigned) tab position after this offset
   1159      */
   1160     float nextTab(float h) {
   1161         if (mTabs != null) {
   1162             return mTabs.nextTab(h);
   1163         }
   1164         return TabStops.nextDefaultStop(h, TAB_INCREMENT);
   1165     }
   1166 
   1167     private boolean isStretchableWhitespace(int ch) {
   1168         // TODO: Support other stretchable whitespace. (Bug: 34013491)
   1169         return ch == 0x0020 || ch == 0x00A0;
   1170     }
   1171 
   1172     private int nextStretchableSpace(int start, int end) {
   1173         for (int i = start; i < end; i++) {
   1174             final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart);
   1175             if (isStretchableWhitespace(c)) return i;
   1176         }
   1177         return end;
   1178     }
   1179 
   1180     /* Return the number of spaces in the text line, for the purpose of justification */
   1181     private int countStretchableSpaces(int start, int end) {
   1182         int count = 0;
   1183         for (int i = start; i < end; i = nextStretchableSpace(i + 1, end)) {
   1184             count++;
   1185         }
   1186         return count;
   1187     }
   1188 
   1189     // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
   1190     public static boolean isLineEndSpace(char ch) {
   1191         return ch == ' ' || ch == '\t' || ch == 0x1680
   1192                 || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007)
   1193                 || ch == 0x205F || ch == 0x3000;
   1194     }
   1195 
   1196     private static final int TAB_INCREMENT = 20;
   1197 }
   1198