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