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