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