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