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