OSDN Git Service

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