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