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.annotations.VisibleForTesting;
     32 import com.android.internal.util.ArrayUtils;
     33 
     34 import java.util.ArrayList;
     35 
     36 /**
     37  * Represents a line of styled text, for measuring in visual order and
     38  * for rendering.
     39  *
     40  * <p>Get a new instance using obtain(), and when finished with it, return it
     41  * to the pool using recycle().
     42  *
     43  * <p>Call set to prepare the instance for use, then either draw, measure,
     44  * metrics, or caretToLeftRightOf.
     45  *
     46  * @hide
     47  */
     48 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     49 public class TextLine {
     50     private static final boolean DEBUG = false;
     51 
     52     private TextPaint mPaint;
     53     private CharSequence mText;
     54     private int mStart;
     55     private int mLen;
     56     private int mDir;
     57     private Directions mDirections;
     58     private boolean mHasTabs;
     59     private TabStops mTabs;
     60     private char[] mChars;
     61     private boolean mCharsValid;
     62     private Spanned mSpanned;
     63     private PrecomputedText mComputed;
     64 
     65     // Additional width of whitespace for justification. This value is per whitespace, thus
     66     // the line width will increase by mAddedWidth x (number of stretchable whitespaces).
     67     private float mAddedWidth;
     68 
     69     private final TextPaint mWorkPaint = new TextPaint();
     70     private final TextPaint mActivePaint = new TextPaint();
     71     private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =
     72             new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);
     73     private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =
     74             new SpanSet<CharacterStyle>(CharacterStyle.class);
     75     private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =
     76             new SpanSet<ReplacementSpan>(ReplacementSpan.class);
     77 
     78     private final DecorationInfo mDecorationInfo = new DecorationInfo();
     79     private final ArrayList<DecorationInfo> mDecorations = new ArrayList<>();
     80 
     81     private static final TextLine[] sCached = new TextLine[3];
     82 
     83     /**
     84      * Returns a new TextLine from the shared pool.
     85      *
     86      * @return an uninitialized TextLine
     87      */
     88     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     89     public static TextLine obtain() {
     90         TextLine tl;
     91         synchronized (sCached) {
     92             for (int i = sCached.length; --i >= 0;) {
     93                 if (sCached[i] != null) {
     94                     tl = sCached[i];
     95                     sCached[i] = null;
     96                     return tl;
     97                 }
     98             }
     99         }
    100         tl = new TextLine();
    101         if (DEBUG) {
    102             Log.v("TLINE", "new: " + tl);
    103         }
    104         return tl;
    105     }
    106 
    107     /**
    108      * Puts a TextLine back into the shared pool. Do not use this TextLine once
    109      * it has been returned.
    110      * @param tl the textLine
    111      * @return null, as a convenience from clearing references to the provided
    112      * TextLine
    113      */
    114     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    115     public static TextLine recycle(TextLine tl) {
    116         tl.mText = null;
    117         tl.mPaint = null;
    118         tl.mDirections = null;
    119         tl.mSpanned = null;
    120         tl.mTabs = null;
    121         tl.mChars = null;
    122         tl.mComputed = null;
    123 
    124         tl.mMetricAffectingSpanSpanSet.recycle();
    125         tl.mCharacterStyleSpanSet.recycle();
    126         tl.mReplacementSpanSpanSet.recycle();
    127 
    128         synchronized(sCached) {
    129             for (int i = 0; i < sCached.length; ++i) {
    130                 if (sCached[i] == null) {
    131                     sCached[i] = tl;
    132                     break;
    133                 }
    134             }
    135         }
    136         return null;
    137     }
    138 
    139     /**
    140      * Initializes a TextLine and prepares it for use.
    141      *
    142      * @param paint the base paint for the line
    143      * @param text the text, can be Styled
    144      * @param start the start of the line relative to the text
    145      * @param limit the limit of the line relative to the text
    146      * @param dir the paragraph direction of this line
    147      * @param directions the directions information of this line
    148      * @param hasTabs true if the line might contain tabs
    149      * @param tabStops the tabStops. Can be null.
    150      */
    151     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    152     public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
    153             Directions directions, boolean hasTabs, TabStops tabStops) {
    154         mPaint = paint;
    155         mText = text;
    156         mStart = start;
    157         mLen = limit - start;
    158         mDir = dir;
    159         mDirections = directions;
    160         if (mDirections == null) {
    161             throw new IllegalArgumentException("Directions cannot be null");
    162         }
    163         mHasTabs = hasTabs;
    164         mSpanned = null;
    165 
    166         boolean hasReplacement = false;
    167         if (text instanceof Spanned) {
    168             mSpanned = (Spanned) text;
    169             mReplacementSpanSpanSet.init(mSpanned, start, limit);
    170             hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
    171         }
    172 
    173         mComputed = null;
    174         if (text instanceof PrecomputedText) {
    175             // Here, no need to check line break strategy or hyphenation frequency since there is no
    176             // line break concept here.
    177             mComputed = (PrecomputedText) text;
    178             if (!mComputed.getParams().getTextPaint().equalsForTextMeasurement(paint)) {
    179                 mComputed = null;
    180             }
    181         }
    182 
    183         mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT;
    184 
    185         if (mCharsValid) {
    186             if (mChars == null || mChars.length < mLen) {
    187                 mChars = ArrayUtils.newUnpaddedCharArray(mLen);
    188             }
    189             TextUtils.getChars(text, start, limit, mChars, 0);
    190             if (hasReplacement) {
    191                 // Handle these all at once so we don't have to do it as we go.
    192                 // Replace the first character of each replacement run with the
    193                 // object-replacement character and the remainder with zero width
    194                 // non-break space aka BOM.  Cursor movement code skips these
    195                 // zero-width characters.
    196                 char[] chars = mChars;
    197                 for (int i = start, inext; i < limit; i = inext) {
    198                     inext = mReplacementSpanSpanSet.getNextTransition(i, limit);
    199                     if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)) {
    200                         // transition into a span
    201                         chars[i - start] = '\ufffc';
    202                         for (int j = i - start + 1, e = inext - start; j < e; ++j) {
    203                             chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
    204                         }
    205                     }
    206                 }
    207             }
    208         }
    209         mTabs = tabStops;
    210         mAddedWidth = 0;
    211     }
    212 
    213     /**
    214      * Justify the line to the given width.
    215      */
    216     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    217     public void justify(float justifyWidth) {
    218         int end = mLen;
    219         while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) {
    220             end--;
    221         }
    222         final int spaces = countStretchableSpaces(0, end);
    223         if (spaces == 0) {
    224             // There are no stretchable spaces, so we can't help the justification by adding any
    225             // width.
    226             return;
    227         }
    228         final float width = Math.abs(measure(end, false, null));
    229         mAddedWidth = (justifyWidth - width) / spaces;
    230     }
    231 
    232     /**
    233      * Renders the TextLine.
    234      *
    235      * @param c the canvas to render on
    236      * @param x the leading margin position
    237      * @param top the top of the line
    238      * @param y the baseline
    239      * @param bottom the bottom of the line
    240      */
    241     void draw(Canvas c, float x, int top, int y, int bottom) {
    242         if (!mHasTabs) {
    243             if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
    244                 drawRun(c, 0, mLen, false, x, top, y, bottom, false);
    245                 return;
    246             }
    247             if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
    248                 drawRun(c, 0, mLen, true, x, top, y, bottom, false);
    249                 return;
    250             }
    251         }
    252 
    253         float h = 0;
    254         int[] runs = mDirections.mDirections;
    255 
    256         int lastRunIndex = runs.length - 2;
    257         for (int i = 0; i < runs.length; i += 2) {
    258             int runStart = runs[i];
    259             int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
    260             if (runLimit > mLen) {
    261                 runLimit = mLen;
    262             }
    263             boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
    264 
    265             int segstart = runStart;
    266             for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
    267                 int codept = 0;
    268                 if (mHasTabs && j < runLimit) {
    269                     codept = mChars[j];
    270                     if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
    271                         codept = Character.codePointAt(mChars, j);
    272                         if (codept > 0xFFFF) {
    273                             ++j;
    274                             continue;
    275                         }
    276                     }
    277                 }
    278 
    279                 if (j == runLimit || codept == '\t') {
    280                     h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom,
    281                             i != lastRunIndex || j != mLen);
    282 
    283                     if (codept == '\t') {
    284                         h = mDir * nextTab(h * mDir);
    285                     }
    286                     segstart = j + 1;
    287                 }
    288             }
    289         }
    290     }
    291 
    292     /**
    293      * Returns metrics information for the entire line.
    294      *
    295      * @param fmi receives font metrics information, can be null
    296      * @return the signed width of the line
    297      */
    298     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    299     public float metrics(FontMetricsInt fmi) {
    300         return measure(mLen, false, fmi);
    301     }
    302 
    303     /**
    304      * Returns information about a position on the line.
    305      *
    306      * @param offset the line-relative character offset, between 0 and the
    307      * line length, inclusive
    308      * @param trailing true to measure the trailing edge of the character
    309      * before offset, false to measure the leading edge of the character
    310      * at offset.
    311      * @param fmi receives metrics information about the requested
    312      * character, can be null.
    313      * @return the signed offset from the leading margin to the requested
    314      * character edge.
    315      */
    316     float measure(int offset, boolean trailing, FontMetricsInt fmi) {
    317         int target = trailing ? offset - 1 : offset;
    318         if (target < 0) {
    319             return 0;
    320         }
    321 
    322         float h = 0;
    323 
    324         if (!mHasTabs) {
    325             if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
    326                 return measureRun(0, offset, mLen, false, fmi);
    327             }
    328             if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
    329                 return measureRun(0, offset, mLen, true, fmi);
    330             }
    331         }
    332 
    333         char[] chars = mChars;
    334         int[] runs = mDirections.mDirections;
    335         for (int i = 0; i < runs.length; i += 2) {
    336             int runStart = runs[i];
    337             int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
    338             if (runLimit > mLen) {
    339                 runLimit = mLen;
    340             }
    341             boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
    342 
    343             int segstart = runStart;
    344             for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
    345                 int codept = 0;
    346                 if (mHasTabs && j < runLimit) {
    347                     codept = chars[j];
    348                     if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
    349                         codept = Character.codePointAt(chars, j);
    350                         if (codept > 0xFFFF) {
    351                             ++j;
    352                             continue;
    353                         }
    354                     }
    355                 }
    356 
    357                 if (j == runLimit || codept == '\t') {
    358                     boolean inSegment = target >= segstart && target < j;
    359 
    360                     boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
    361                     if (inSegment && advance) {
    362                         return h + measureRun(segstart, offset, j, runIsRtl, fmi);
    363                     }
    364 
    365                     float w = measureRun(segstart, j, j, runIsRtl, fmi);
    366                     h += advance ? w : -w;
    367 
    368                     if (inSegment) {
    369                         return h + measureRun(segstart, offset, j, runIsRtl, null);
    370                     }
    371 
    372                     if (codept == '\t') {
    373                         if (offset == j) {
    374                             return h;
    375                         }
    376                         h = mDir * nextTab(h * mDir);
    377                         if (target == j) {
    378                             return h;
    379                         }
    380                     }
    381 
    382                     segstart = j + 1;
    383                 }
    384             }
    385         }
    386 
    387         return h;
    388     }
    389 
    390     /**
    391      * @see #measure(int, boolean, FontMetricsInt)
    392      * @return The measure results for all possible offsets
    393      */
    394     float[] measureAllOffsets(boolean[] trailing, FontMetricsInt fmi) {
    395         float[] measurement = new float[mLen + 1];
    396 
    397         int[] target = new int[mLen + 1];
    398         for (int offset = 0; offset < target.length; ++offset) {
    399             target[offset] = trailing[offset] ? offset - 1 : offset;
    400         }
    401         if (target[0] < 0) {
    402             measurement[0] = 0;
    403         }
    404 
    405         float h = 0;
    406 
    407         if (!mHasTabs) {
    408             if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
    409                 for (int offset = 0; offset <= mLen; ++offset) {
    410                     measurement[offset] = measureRun(0, offset, mLen, false, fmi);
    411                 }
    412                 return measurement;
    413             }
    414             if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
    415                 for (int offset = 0; offset <= mLen; ++offset) {
    416                     measurement[offset] = measureRun(0, offset, mLen, true, fmi);
    417                 }
    418                 return measurement;
    419             }
    420         }
    421 
    422         char[] chars = mChars;
    423         int[] runs = mDirections.mDirections;
    424         for (int i = 0; i < runs.length; i += 2) {
    425             int runStart = runs[i];
    426             int runLimit = runStart + (runs[i + 1] & Layout.RUN_LENGTH_MASK);
    427             if (runLimit > mLen) {
    428                 runLimit = mLen;
    429             }
    430             boolean runIsRtl = (runs[i + 1] & Layout.RUN_RTL_FLAG) != 0;
    431 
    432             int segstart = runStart;
    433             for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; ++j) {
    434                 int codept = 0;
    435                 if (mHasTabs && j < runLimit) {
    436                     codept = chars[j];
    437                     if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
    438                         codept = Character.codePointAt(chars, j);
    439                         if (codept > 0xFFFF) {
    440                             ++j;
    441                             continue;
    442                         }
    443                     }
    444                 }
    445 
    446                 if (j == runLimit || codept == '\t') {
    447                     float oldh = h;
    448                     boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
    449                     float w = measureRun(segstart, j, j, runIsRtl, fmi);
    450                     h += advance ? w : -w;
    451 
    452                     float baseh = advance ? oldh : h;
    453                     FontMetricsInt crtfmi = advance ? fmi : null;
    454                     for (int offset = segstart; offset <= j && offset <= mLen; ++offset) {
    455                         if (target[offset] >= segstart && target[offset] < j) {
    456                             measurement[offset] =
    457                                     baseh + measureRun(segstart, offset, j, runIsRtl, crtfmi);
    458                         }
    459                     }
    460 
    461                     if (codept == '\t') {
    462                         if (target[j] == j) {
    463                             measurement[j] = h;
    464                         }
    465                         h = mDir * nextTab(h * mDir);
    466                         if (target[j + 1] == j) {
    467                             measurement[j + 1] =  h;
    468                         }
    469                     }
    470 
    471                     segstart = j + 1;
    472                 }
    473             }
    474         }
    475         if (target[mLen] == mLen) {
    476             measurement[mLen] = h;
    477         }
    478 
    479         return measurement;
    480     }
    481 
    482     /**
    483      * Draws a unidirectional (but possibly multi-styled) run of text.
    484      *
    485      *
    486      * @param c the canvas to draw on
    487      * @param start the line-relative start
    488      * @param limit the line-relative limit
    489      * @param runIsRtl true if the run is right-to-left
    490      * @param x the position of the run that is closest to the leading margin
    491      * @param top the top of the line
    492      * @param y the baseline
    493      * @param bottom the bottom of the line
    494      * @param needWidth true if the width value is required.
    495      * @return the signed width of the run, based on the paragraph direction.
    496      * Only valid if needWidth is true.
    497      */
    498     private float drawRun(Canvas c, int start,
    499             int limit, boolean runIsRtl, float x, int top, int y, int bottom,
    500             boolean needWidth) {
    501 
    502         if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
    503             float w = -measureRun(start, limit, limit, runIsRtl, null);
    504             handleRun(start, limit, limit, runIsRtl, c, x + w, top,
    505                     y, bottom, null, false);
    506             return w;
    507         }
    508 
    509         return handleRun(start, limit, limit, runIsRtl, c, x, top,
    510                 y, bottom, null, needWidth);
    511     }
    512 
    513     /**
    514      * Measures a unidirectional (but possibly multi-styled) run of text.
    515      *
    516      *
    517      * @param start the line-relative start of the run
    518      * @param offset the offset to measure to, between start and limit inclusive
    519      * @param limit the line-relative limit of the run
    520      * @param runIsRtl true if the run is right-to-left
    521      * @param fmi receives metrics information about the requested
    522      * run, can be null.
    523      * @return the signed width from the start of the run to the leading edge
    524      * of the character at offset, based on the run (not paragraph) direction
    525      */
    526     private float measureRun(int start, int offset, int limit, boolean runIsRtl,
    527             FontMetricsInt fmi) {
    528         return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true);
    529     }
    530 
    531     /**
    532      * Walk the cursor through this line, skipping conjuncts and
    533      * zero-width characters.
    534      *
    535      * <p>This function cannot properly walk the cursor off the ends of the line
    536      * since it does not know about any shaping on the previous/following line
    537      * that might affect the cursor position. Callers must either avoid these
    538      * situations or handle the result specially.
    539      *
    540      * @param cursor the starting position of the cursor, between 0 and the
    541      * length of the line, inclusive
    542      * @param toLeft true if the caret is moving to the left.
    543      * @return the new offset.  If it is less than 0 or greater than the length
    544      * of the line, the previous/following line should be examined to get the
    545      * actual offset.
    546      */
    547     int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
    548         // 1) The caret marks the leading edge of a character. The character
    549         // logically before it might be on a different level, and the active caret
    550         // position is on the character at the lower level. If that character
    551         // was the previous character, the caret is on its trailing edge.
    552         // 2) Take this character/edge and move it in the indicated direction.
    553         // This gives you a new character and a new edge.
    554         // 3) This position is between two visually adjacent characters.  One of
    555         // these might be at a lower level.  The active position is on the
    556         // character at the lower level.
    557         // 4) If the active position is on the trailing edge of the character,
    558         // the new caret position is the following logical character, else it
    559         // is the character.
    560 
    561         int lineStart = 0;
    562         int lineEnd = mLen;
    563         boolean paraIsRtl = mDir == -1;
    564         int[] runs = mDirections.mDirections;
    565 
    566         int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
    567         boolean trailing = false;
    568 
    569         if (cursor == lineStart) {
    570             runIndex = -2;
    571         } else if (cursor == lineEnd) {
    572             runIndex = runs.length;
    573         } else {
    574           // First, get information about the run containing the character with
    575           // the active caret.
    576           for (runIndex = 0; runIndex < runs.length; runIndex += 2) {
    577             runStart = lineStart + runs[runIndex];
    578             if (cursor >= runStart) {
    579               runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK);
    580               if (runLimit > lineEnd) {
    581                   runLimit = lineEnd;
    582               }
    583               if (cursor < runLimit) {
    584                 runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
    585                     Layout.RUN_LEVEL_MASK;
    586                 if (cursor == runStart) {
    587                   // The caret is on a run boundary, see if we should
    588                   // use the position on the trailing edge of the previous
    589                   // logical character instead.
    590                   int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;
    591                   int pos = cursor - 1;
    592                   for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {
    593                     prevRunStart = lineStart + runs[prevRunIndex];
    594                     if (pos >= prevRunStart) {
    595                       prevRunLimit = prevRunStart +
    596                           (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK);
    597                       if (prevRunLimit > lineEnd) {
    598                           prevRunLimit = lineEnd;
    599                       }
    600                       if (pos < prevRunLimit) {
    601                         prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT)
    602                             & Layout.RUN_LEVEL_MASK;
    603                         if (prevRunLevel < runLevel) {
    604                           // Start from logically previous character.
    605                           runIndex = prevRunIndex;
    606                           runLevel = prevRunLevel;
    607                           runStart = prevRunStart;
    608                           runLimit = prevRunLimit;
    609                           trailing = true;
    610                           break;
    611                         }
    612                       }
    613                     }
    614                   }
    615                 }
    616                 break;
    617               }
    618             }
    619           }
    620 
    621           // caret might be == lineEnd.  This is generally a space or paragraph
    622           // separator and has an associated run, but might be the end of
    623           // text, in which case it doesn't.  If that happens, we ran off the
    624           // end of the run list, and runIndex == runs.length.  In this case,
    625           // we are at a run boundary so we skip the below test.
    626           if (runIndex != runs.length) {
    627               boolean runIsRtl = (runLevel & 0x1) != 0;
    628               boolean advance = toLeft == runIsRtl;
    629               if (cursor != (advance ? runLimit : runStart) || advance != trailing) {
    630                   // Moving within or into the run, so we can move logically.
    631                   newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit,
    632                           runIsRtl, cursor, advance);
    633                   // If the new position is internal to the run, we're at the strong
    634                   // position already so we're finished.
    635                   if (newCaret != (advance ? runLimit : runStart)) {
    636                       return newCaret;
    637                   }
    638               }
    639           }
    640         }
    641 
    642         // If newCaret is -1, we're starting at a run boundary and crossing
    643         // into another run. Otherwise we've arrived at a run boundary, and
    644         // need to figure out which character to attach to.  Note we might
    645         // need to run this twice, if we cross a run boundary and end up at
    646         // another run boundary.
    647         while (true) {
    648           boolean advance = toLeft == paraIsRtl;
    649           int otherRunIndex = runIndex + (advance ? 2 : -2);
    650           if (otherRunIndex >= 0 && otherRunIndex < runs.length) {
    651             int otherRunStart = lineStart + runs[otherRunIndex];
    652             int otherRunLimit = otherRunStart +
    653             (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK);
    654             if (otherRunLimit > lineEnd) {
    655                 otherRunLimit = lineEnd;
    656             }
    657             int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
    658                 Layout.RUN_LEVEL_MASK;
    659             boolean otherRunIsRtl = (otherRunLevel & 1) != 0;
    660 
    661             advance = toLeft == otherRunIsRtl;
    662             if (newCaret == -1) {
    663                 newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart,
    664                         otherRunLimit, otherRunIsRtl,
    665                         advance ? otherRunStart : otherRunLimit, advance);
    666                 if (newCaret == (advance ? otherRunLimit : otherRunStart)) {
    667                     // Crossed and ended up at a new boundary,
    668                     // repeat a second and final time.
    669                     runIndex = otherRunIndex;
    670                     runLevel = otherRunLevel;
    671                     continue;
    672                 }
    673                 break;
    674             }
    675 
    676             // The new caret is at a boundary.
    677             if (otherRunLevel < runLevel) {
    678               // The strong character is in the other run.
    679               newCaret = advance ? otherRunStart : otherRunLimit;
    680             }
    681             break;
    682           }
    683 
    684           if (newCaret == -1) {
    685               // We're walking off the end of the line.  The paragraph
    686               // level is always equal to or lower than any internal level, so
    687               // the boundaries get the strong caret.
    688               newCaret = advance ? mLen + 1 : -1;
    689               break;
    690           }
    691 
    692           // Else we've arrived at the end of the line.  That's a strong position.
    693           // We might have arrived here by crossing over a run with no internal
    694           // breaks and dropping out of the above loop before advancing one final
    695           // time, so reset the caret.
    696           // Note, we use '<=' below to handle a situation where the only run
    697           // on the line is a counter-directional run.  If we're not advancing,
    698           // we can end up at the 'lineEnd' position but the caret we want is at
    699           // the lineStart.
    700           if (newCaret <= lineEnd) {
    701               newCaret = advance ? lineEnd : lineStart;
    702           }
    703           break;
    704         }
    705 
    706         return newCaret;
    707     }
    708 
    709     /**
    710      * Returns the next valid offset within this directional run, skipping
    711      * conjuncts and zero-width characters.  This should not be called to walk
    712      * off the end of the line, since the returned values might not be valid
    713      * on neighboring lines.  If the returned offset is less than zero or
    714      * greater than the line length, the offset should be recomputed on the
    715      * preceding or following line, respectively.
    716      *
    717      * @param runIndex the run index
    718      * @param runStart the start of the run
    719      * @param runLimit the limit of the run
    720      * @param runIsRtl true if the run is right-to-left
    721      * @param offset the offset
    722      * @param after true if the new offset should logically follow the provided
    723      * offset
    724      * @return the new offset
    725      */
    726     private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit,
    727             boolean runIsRtl, int offset, boolean after) {
    728 
    729         if (runIndex < 0 || offset == (after ? mLen : 0)) {
    730             // Walking off end of line.  Since we don't know
    731             // what cursor positions are available on other lines, we can't
    732             // return accurate values.  These are a guess.
    733             if (after) {
    734                 return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;
    735             }
    736             return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;
    737         }
    738 
    739         TextPaint wp = mWorkPaint;
    740         wp.set(mPaint);
    741         wp.setWordSpacing(mAddedWidth);
    742 
    743         int spanStart = runStart;
    744         int spanLimit;
    745         if (mSpanned == null) {
    746             spanLimit = runLimit;
    747         } else {
    748             int target = after ? offset + 1 : offset;
    749             int limit = mStart + runLimit;
    750             while (true) {
    751                 spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,
    752                         MetricAffectingSpan.class) - mStart;
    753                 if (spanLimit >= target) {
    754                     break;
    755                 }
    756                 spanStart = spanLimit;
    757             }
    758 
    759             MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,
    760                     mStart + spanLimit, MetricAffectingSpan.class);
    761             spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);
    762 
    763             if (spans.length > 0) {
    764                 ReplacementSpan replacement = null;
    765                 for (int j = 0; j < spans.length; j++) {
    766                     MetricAffectingSpan span = spans[j];
    767                     if (span instanceof ReplacementSpan) {
    768                         replacement = (ReplacementSpan)span;
    769                     } else {
    770                         span.updateMeasureState(wp);
    771                     }
    772                 }
    773 
    774                 if (replacement != null) {
    775                     // If we have a replacement span, we're moving either to
    776                     // the start or end of this span.
    777                     return after ? spanLimit : spanStart;
    778                 }
    779             }
    780         }
    781 
    782         int dir = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
    783         int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;
    784         if (mCharsValid) {
    785             return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,
    786                     dir, offset, cursorOpt);
    787         } else {
    788             return wp.getTextRunCursor(mText, mStart + spanStart,
    789                     mStart + spanLimit, dir, mStart + offset, cursorOpt) - mStart;
    790         }
    791     }
    792 
    793     /**
    794      * @param wp
    795      */
    796     private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
    797         final int previousTop     = fmi.top;
    798         final int previousAscent  = fmi.ascent;
    799         final int previousDescent = fmi.descent;
    800         final int previousBottom  = fmi.bottom;
    801         final int previousLeading = fmi.leading;
    802 
    803         wp.getFontMetricsInt(fmi);
    804 
    805         updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
    806                 previousLeading);
    807     }
    808 
    809     static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
    810             int previousDescent, int previousBottom, int previousLeading) {
    811         fmi.top     = Math.min(fmi.top,     previousTop);
    812         fmi.ascent  = Math.min(fmi.ascent,  previousAscent);
    813         fmi.descent = Math.max(fmi.descent, previousDescent);
    814         fmi.bottom  = Math.max(fmi.bottom,  previousBottom);
    815         fmi.leading = Math.max(fmi.leading, previousLeading);
    816     }
    817 
    818     private static void drawStroke(TextPaint wp, Canvas c, int color, float position,
    819             float thickness, float xleft, float xright, float baseline) {
    820         final float strokeTop = baseline + wp.baselineShift + position;
    821 
    822         final int previousColor = wp.getColor();
    823         final Paint.Style previousStyle = wp.getStyle();
    824         final boolean previousAntiAlias = wp.isAntiAlias();
    825 
    826         wp.setStyle(Paint.Style.FILL);
    827         wp.setAntiAlias(true);
    828 
    829         wp.setColor(color);
    830         c.drawRect(xleft, strokeTop, xright, strokeTop + thickness, wp);
    831 
    832         wp.setStyle(previousStyle);
    833         wp.setColor(previousColor);
    834         wp.setAntiAlias(previousAntiAlias);
    835     }
    836 
    837     private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
    838             boolean runIsRtl, int offset) {
    839         if (mCharsValid) {
    840             return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset);
    841         } else {
    842             final int delta = mStart;
    843             if (mComputed == null) {
    844                 // TODO: Enable measured getRunAdvance for ReplacementSpan and RTL text.
    845                 return wp.getRunAdvance(mText, delta + start, delta + end,
    846                         delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
    847             } else {
    848                 return mComputed.getWidth(start + delta, end + delta);
    849             }
    850         }
    851     }
    852 
    853     /**
    854      * Utility function for measuring and rendering text.  The text must
    855      * not include a tab.
    856      *
    857      * @param wp the working paint
    858      * @param start the start of the text
    859      * @param end the end of the text
    860      * @param runIsRtl true if the run is right-to-left
    861      * @param c the canvas, can be null if rendering is not needed
    862      * @param x the edge of the run 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 run is needed
    868      * @param offset the offset for the purpose of measuring
    869      * @param decorations the list of locations and paremeters for drawing decorations
    870      * @return the signed width of the run based on the run direction; only
    871      * valid if needWidth is true
    872      */
    873     private float handleText(TextPaint wp, int start, int end,
    874             int contextStart, int contextEnd, boolean runIsRtl,
    875             Canvas c, float x, int top, int y, int bottom,
    876             FontMetricsInt fmi, boolean needWidth, int offset,
    877             @Nullable ArrayList<DecorationInfo> decorations) {
    878 
    879         wp.setWordSpacing(mAddedWidth);
    880         // Get metrics first (even for empty strings or "0" width runs)
    881         if (fmi != null) {
    882             expandMetricsFromPaint(fmi, wp);
    883         }
    884 
    885         // No need to do anything if the run width is "0"
    886         if (end == start) {
    887             return 0f;
    888         }
    889 
    890         float totalWidth = 0;
    891 
    892         final int numDecorations = decorations == null ? 0 : decorations.size();
    893         if (needWidth || (c != null && (wp.bgColor != 0 || numDecorations != 0 || runIsRtl))) {
    894             totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset);
    895         }
    896 
    897         if (c != null) {
    898             final float leftX, rightX;
    899             if (runIsRtl) {
    900                 leftX = x - totalWidth;
    901                 rightX = x;
    902             } else {
    903                 leftX = x;
    904                 rightX = x + totalWidth;
    905             }
    906 
    907             if (wp.bgColor != 0) {
    908                 int previousColor = wp.getColor();
    909                 Paint.Style previousStyle = wp.getStyle();
    910 
    911                 wp.setColor(wp.bgColor);
    912                 wp.setStyle(Paint.Style.FILL);
    913                 c.drawRect(leftX, top, rightX, bottom, wp);
    914 
    915                 wp.setStyle(previousStyle);
    916                 wp.setColor(previousColor);
    917             }
    918 
    919             if (numDecorations != 0) {
    920                 for (int i = 0; i < numDecorations; i++) {
    921                     final DecorationInfo info = decorations.get(i);
    922 
    923                     final int decorationStart = Math.max(info.start, start);
    924                     final int decorationEnd = Math.min(info.end, offset);
    925                     float decorationStartAdvance = getRunAdvance(
    926                             wp, start, end, contextStart, contextEnd, runIsRtl, decorationStart);
    927                     float decorationEndAdvance = getRunAdvance(
    928                             wp, start, end, contextStart, contextEnd, runIsRtl, decorationEnd);
    929                     final float decorationXLeft, decorationXRight;
    930                     if (runIsRtl) {
    931                         decorationXLeft = rightX - decorationEndAdvance;
    932                         decorationXRight = rightX - decorationStartAdvance;
    933                     } else {
    934                         decorationXLeft = leftX + decorationStartAdvance;
    935                         decorationXRight = leftX + decorationEndAdvance;
    936                     }
    937 
    938                     // Theoretically, there could be cases where both Paint's and TextPaint's
    939                     // setUnderLineText() are called. For backward compatibility, we need to draw
    940                     // both underlines, the one with custom color first.
    941                     if (info.underlineColor != 0) {
    942                         drawStroke(wp, c, info.underlineColor, wp.getUnderlinePosition(),
    943                                 info.underlineThickness, decorationXLeft, decorationXRight, y);
    944                     }
    945                     if (info.isUnderlineText) {
    946                         final float thickness =
    947                                 Math.max(wp.getUnderlineThickness(), 1.0f);
    948                         drawStroke(wp, c, wp.getColor(), wp.getUnderlinePosition(), thickness,
    949                                 decorationXLeft, decorationXRight, y);
    950                     }
    951 
    952                     if (info.isStrikeThruText) {
    953                         final float thickness =
    954                                 Math.max(wp.getStrikeThruThickness(), 1.0f);
    955                         drawStroke(wp, c, wp.getColor(), wp.getStrikeThruPosition(), thickness,
    956                                 decorationXLeft, decorationXRight, y);
    957                     }
    958                 }
    959             }
    960 
    961             drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
    962                     leftX, y + wp.baselineShift);
    963         }
    964 
    965         return runIsRtl ? -totalWidth : totalWidth;
    966     }
    967 
    968     /**
    969      * Utility function for measuring and rendering a replacement.
    970      *
    971      *
    972      * @param replacement the replacement
    973      * @param wp the work paint
    974      * @param start the start of the run
    975      * @param limit the limit of the run
    976      * @param runIsRtl true if the run is right-to-left
    977      * @param c the canvas, can be null if not rendering
    978      * @param x the edge of the replacement closest to the leading margin
    979      * @param top the top of the line
    980      * @param y the baseline
    981      * @param bottom the bottom of the line
    982      * @param fmi receives metrics information, can be null
    983      * @param needWidth true if the width of the replacement is needed
    984      * @return the signed width of the run based on the run direction; only
    985      * valid if needWidth is true
    986      */
    987     private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
    988             int start, int limit, boolean runIsRtl, Canvas c,
    989             float x, int top, int y, int bottom, FontMetricsInt fmi,
    990             boolean needWidth) {
    991 
    992         float ret = 0;
    993 
    994         int textStart = mStart + start;
    995         int textLimit = mStart + limit;
    996 
    997         if (needWidth || (c != null && runIsRtl)) {
    998             int previousTop = 0;
    999             int previousAscent = 0;
   1000             int previousDescent = 0;
   1001             int previousBottom = 0;
   1002             int previousLeading = 0;
   1003 
   1004             boolean needUpdateMetrics = (fmi != null);
   1005 
   1006             if (needUpdateMetrics) {
   1007                 previousTop     = fmi.top;
   1008                 previousAscent  = fmi.ascent;
   1009                 previousDescent = fmi.descent;
   1010                 previousBottom  = fmi.bottom;
   1011                 previousLeading = fmi.leading;
   1012             }
   1013 
   1014             ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
   1015 
   1016             if (needUpdateMetrics) {
   1017                 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
   1018                         previousLeading);
   1019             }
   1020         }
   1021 
   1022         if (c != null) {
   1023             if (runIsRtl) {
   1024                 x -= ret;
   1025             }
   1026             replacement.draw(c, mText, textStart, textLimit,
   1027                     x, top, y, bottom, wp);
   1028         }
   1029 
   1030         return runIsRtl ? -ret : ret;
   1031     }
   1032 
   1033     private int adjustHyphenEdit(int start, int limit, int hyphenEdit) {
   1034         int result = hyphenEdit;
   1035         // Only draw hyphens on first or last run in line. Disable them otherwise.
   1036         if (start > 0) { // not the first run
   1037             result &= ~Paint.HYPHENEDIT_MASK_START_OF_LINE;
   1038         }
   1039         if (limit < mLen) { // not the last run
   1040             result &= ~Paint.HYPHENEDIT_MASK_END_OF_LINE;
   1041         }
   1042         return result;
   1043     }
   1044 
   1045     private static final class DecorationInfo {
   1046         public boolean isStrikeThruText;
   1047         public boolean isUnderlineText;
   1048         public int underlineColor;
   1049         public float underlineThickness;
   1050         public int start = -1;
   1051         public int end = -1;
   1052 
   1053         public boolean hasDecoration() {
   1054             return isStrikeThruText || isUnderlineText || underlineColor != 0;
   1055         }
   1056 
   1057         // Copies the info, but not the start and end range.
   1058         public DecorationInfo copyInfo() {
   1059             final DecorationInfo copy = new DecorationInfo();
   1060             copy.isStrikeThruText = isStrikeThruText;
   1061             copy.isUnderlineText = isUnderlineText;
   1062             copy.underlineColor = underlineColor;
   1063             copy.underlineThickness = underlineThickness;
   1064             return copy;
   1065         }
   1066     }
   1067 
   1068     private void extractDecorationInfo(@NonNull TextPaint paint, @NonNull DecorationInfo info) {
   1069         info.isStrikeThruText = paint.isStrikeThruText();
   1070         if (info.isStrikeThruText) {
   1071             paint.setStrikeThruText(false);
   1072         }
   1073         info.isUnderlineText = paint.isUnderlineText();
   1074         if (info.isUnderlineText) {
   1075             paint.setUnderlineText(false);
   1076         }
   1077         info.underlineColor = paint.underlineColor;
   1078         info.underlineThickness = paint.underlineThickness;
   1079         paint.setUnderlineText(0, 0.0f);
   1080     }
   1081 
   1082     /**
   1083      * Utility function for handling a unidirectional run.  The run must not
   1084      * contain tabs but can contain styles.
   1085      *
   1086      *
   1087      * @param start the line-relative start of the run
   1088      * @param measureLimit the offset to measure to, between start and limit inclusive
   1089      * @param limit the limit of the run
   1090      * @param runIsRtl true if the run is right-to-left
   1091      * @param c the canvas, can be null
   1092      * @param x the end of the run closest to the leading margin
   1093      * @param top the top of the line
   1094      * @param y the baseline
   1095      * @param bottom the bottom of the line
   1096      * @param fmi receives metrics information, can be null
   1097      * @param needWidth true if the width is required
   1098      * @return the signed width of the run based on the run direction; only
   1099      * valid if needWidth is true
   1100      */
   1101     private float handleRun(int start, int measureLimit,
   1102             int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
   1103             int bottom, FontMetricsInt fmi, boolean needWidth) {
   1104 
   1105         if (measureLimit < start || measureLimit > limit) {
   1106             throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
   1107                     + "start (" + start + ") and limit (" + limit + ") bounds");
   1108         }
   1109 
   1110         // Case of an empty line, make sure we update fmi according to mPaint
   1111         if (start == measureLimit) {
   1112             final TextPaint wp = mWorkPaint;
   1113             wp.set(mPaint);
   1114             if (fmi != null) {
   1115                 expandMetricsFromPaint(fmi, wp);
   1116             }
   1117             return 0f;
   1118         }
   1119 
   1120         final boolean needsSpanMeasurement;
   1121         if (mSpanned == null) {
   1122             needsSpanMeasurement = false;
   1123         } else {
   1124             mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
   1125             mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
   1126             needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0
   1127                     || mCharacterStyleSpanSet.numberOfSpans != 0;
   1128         }
   1129 
   1130         if (!needsSpanMeasurement) {
   1131             final TextPaint wp = mWorkPaint;
   1132             wp.set(mPaint);
   1133             wp.setHyphenEdit(adjustHyphenEdit(start, limit, wp.getHyphenEdit()));
   1134             return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,
   1135                     y, bottom, fmi, needWidth, measureLimit, null);
   1136         }
   1137 
   1138         // Shaping needs to take into account context up to metric boundaries,
   1139         // but rendering needs to take into account character style boundaries.
   1140         // So we iterate through metric runs to get metric bounds,
   1141         // then within each metric run iterate through character style runs
   1142         // for the run bounds.
   1143         final float originalX = x;
   1144         for (int i = start, inext; i < measureLimit; i = inext) {
   1145             final TextPaint wp = mWorkPaint;
   1146             wp.set(mPaint);
   1147 
   1148             inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
   1149                     mStart;
   1150             int mlimit = Math.min(inext, measureLimit);
   1151 
   1152             ReplacementSpan replacement = null;
   1153 
   1154             for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
   1155                 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
   1156                 // empty by construction. This special case in getSpans() explains the >= & <= tests
   1157                 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||
   1158                         (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
   1159                 final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
   1160                 if (span instanceof ReplacementSpan) {
   1161                     replacement = (ReplacementSpan)span;
   1162                 } else {
   1163                     // We might have a replacement that uses the draw
   1164                     // state, otherwise measure state would suffice.
   1165                     span.updateDrawState(wp);
   1166                 }
   1167             }
   1168 
   1169             if (replacement != null) {
   1170                 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
   1171                         bottom, fmi, needWidth || mlimit < measureLimit);
   1172                 continue;
   1173             }
   1174 
   1175             final TextPaint activePaint = mActivePaint;
   1176             activePaint.set(mPaint);
   1177             int activeStart = i;
   1178             int activeEnd = mlimit;
   1179             final DecorationInfo decorationInfo = mDecorationInfo;
   1180             mDecorations.clear();
   1181             for (int j = i, jnext; j < mlimit; j = jnext) {
   1182                 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
   1183                         mStart;
   1184 
   1185                 final int offset = Math.min(jnext, mlimit);
   1186                 wp.set(mPaint);
   1187                 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
   1188                     // Intentionally using >= and <= as explained above
   1189                     if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
   1190                             (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
   1191 
   1192                     final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
   1193                     span.updateDrawState(wp);
   1194                 }
   1195 
   1196                 extractDecorationInfo(wp, decorationInfo);
   1197 
   1198                 if (j == i) {
   1199                     // First chunk of text. We can't handle it yet, since we may need to merge it
   1200                     // with the next chunk. So we just save the TextPaint for future comparisons
   1201                     // and use.
   1202                     activePaint.set(wp);
   1203                 } else if (!wp.hasEqualAttributes(activePaint)) {
   1204                     // The style of the present chunk of text is substantially different from the
   1205                     // style of the previous chunk. We need to handle the active piece of text
   1206                     // and restart with the present chunk.
   1207                     activePaint.setHyphenEdit(adjustHyphenEdit(
   1208                             activeStart, activeEnd, mPaint.getHyphenEdit()));
   1209                     x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
   1210                             top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
   1211                             Math.min(activeEnd, mlimit), mDecorations);
   1212 
   1213                     activeStart = j;
   1214                     activePaint.set(wp);
   1215                     mDecorations.clear();
   1216                 } else {
   1217                     // The present TextPaint is substantially equal to the last TextPaint except
   1218                     // perhaps for decorations. We just need to expand the active piece of text to
   1219                     // include the present chunk, which we always do anyway. We don't need to save
   1220                     // wp to activePaint, since they are already equal.
   1221                 }
   1222 
   1223                 activeEnd = jnext;
   1224                 if (decorationInfo.hasDecoration()) {
   1225                     final DecorationInfo copy = decorationInfo.copyInfo();
   1226                     copy.start = j;
   1227                     copy.end = jnext;
   1228                     mDecorations.add(copy);
   1229                 }
   1230             }
   1231             // Handle the final piece of text.
   1232             activePaint.setHyphenEdit(adjustHyphenEdit(
   1233                     activeStart, activeEnd, mPaint.getHyphenEdit()));
   1234             x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
   1235                     top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
   1236                     Math.min(activeEnd, mlimit), mDecorations);
   1237         }
   1238 
   1239         return x - originalX;
   1240     }
   1241 
   1242     /**
   1243      * Render a text run with the set-up paint.
   1244      *
   1245      * @param c the canvas
   1246      * @param wp the paint used to render the text
   1247      * @param start the start of the run
   1248      * @param end the end of the run
   1249      * @param contextStart the start of context for the run
   1250      * @param contextEnd the end of the context for the run
   1251      * @param runIsRtl true if the run is right-to-left
   1252      * @param x the x position of the left edge of the run
   1253      * @param y the baseline of the run
   1254      */
   1255     private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
   1256             int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
   1257 
   1258         if (mCharsValid) {
   1259             int count = end - start;
   1260             int contextCount = contextEnd - contextStart;
   1261             c.drawTextRun(mChars, start, count, contextStart, contextCount,
   1262                     x, y, runIsRtl, wp);
   1263         } else {
   1264             int delta = mStart;
   1265             c.drawTextRun(mText, delta + start, delta + end,
   1266                     delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
   1267         }
   1268     }
   1269 
   1270     /**
   1271      * Returns the next tab position.
   1272      *
   1273      * @param h the (unsigned) offset from the leading margin
   1274      * @return the (unsigned) tab position after this offset
   1275      */
   1276     float nextTab(float h) {
   1277         if (mTabs != null) {
   1278             return mTabs.nextTab(h);
   1279         }
   1280         return TabStops.nextDefaultStop(h, TAB_INCREMENT);
   1281     }
   1282 
   1283     private boolean isStretchableWhitespace(int ch) {
   1284         // TODO: Support NBSP and other stretchable whitespace (b/34013491 and b/68204709).
   1285         return ch == 0x0020;
   1286     }
   1287 
   1288     /* Return the number of spaces in the text line, for the purpose of justification */
   1289     private int countStretchableSpaces(int start, int end) {
   1290         int count = 0;
   1291         for (int i = start; i < end; i++) {
   1292             final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart);
   1293             if (isStretchableWhitespace(c)) {
   1294                 count++;
   1295             }
   1296         }
   1297         return count;
   1298     }
   1299 
   1300     // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
   1301     public static boolean isLineEndSpace(char ch) {
   1302         return ch == ' ' || ch == '\t' || ch == 0x1680
   1303                 || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007)
   1304                 || ch == 0x205F || ch == 0x3000;
   1305     }
   1306 
   1307     private static final int TAB_INCREMENT = 20;
   1308 }
   1309