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