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.graphics.Bitmap;
20 import android.graphics.Canvas;
21 import android.graphics.Paint;
22 import android.graphics.Paint.FontMetricsInt;
23 import android.graphics.RectF;
24 import android.text.Layout.Directions;
25 import android.text.Layout.TabStops;
26 import android.text.style.CharacterStyle;
27 import android.text.style.MetricAffectingSpan;
28 import android.text.style.ReplacementSpan;
29 import android.util.Log;
30 
31 import com.android.internal.util.ArrayUtils;
32 
33 /**
34  * Represents a line of styled text, for measuring in visual order and
35  * for rendering.
36  *
37  * <p>Get a new instance using obtain(), and when finished with it, return it
38  * to the pool using recycle().
39  *
40  * <p>Call set to prepare the instance for use, then either draw, measure,
41  * metrics, or caretToLeftRightOf.
42  *
43  * @hide
44  */
45 class TextLine {
46     private static final boolean DEBUG = false;
47 
48     private TextPaint mPaint;
49     private CharSequence mText;
50     private int mStart;
51     private int mLen;
52     private int mDir;
53     private Directions mDirections;
54     private boolean mHasTabs;
55     private TabStops mTabs;
56     private char[] mChars;
57     private boolean mCharsValid;
58     private Spanned mSpanned;
59     private final TextPaint mWorkPaint = new TextPaint();
60     private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =
61             new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);
62     private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =
63             new SpanSet<CharacterStyle>(CharacterStyle.class);
64     private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =
65             new SpanSet<ReplacementSpan>(ReplacementSpan.class);
66 
67     private static final TextLine[] sCached = new TextLine[3];
68 
69     /**
70      * Returns a new TextLine from the shared pool.
71      *
72      * @return an uninitialized TextLine
73      */
obtain()74     static TextLine obtain() {
75         TextLine tl;
76         synchronized (sCached) {
77             for (int i = sCached.length; --i >= 0;) {
78                 if (sCached[i] != null) {
79                     tl = sCached[i];
80                     sCached[i] = null;
81                     return tl;
82                 }
83             }
84         }
85         tl = new TextLine();
86         if (DEBUG) {
87             Log.v("TLINE", "new: " + tl);
88         }
89         return tl;
90     }
91 
92     /**
93      * Puts a TextLine back into the shared pool. Do not use this TextLine once
94      * it has been returned.
95      * @param tl the textLine
96      * @return null, as a convenience from clearing references to the provided
97      * TextLine
98      */
recycle(TextLine tl)99     static TextLine recycle(TextLine tl) {
100         tl.mText = null;
101         tl.mPaint = null;
102         tl.mDirections = null;
103         tl.mSpanned = null;
104         tl.mTabs = null;
105         tl.mChars = null;
106 
107         tl.mMetricAffectingSpanSpanSet.recycle();
108         tl.mCharacterStyleSpanSet.recycle();
109         tl.mReplacementSpanSpanSet.recycle();
110 
111         synchronized(sCached) {
112             for (int i = 0; i < sCached.length; ++i) {
113                 if (sCached[i] == null) {
114                     sCached[i] = tl;
115                     break;
116                 }
117             }
118         }
119         return null;
120     }
121 
122     /**
123      * Initializes a TextLine and prepares it for use.
124      *
125      * @param paint the base paint for the line
126      * @param text the text, can be Styled
127      * @param start the start of the line relative to the text
128      * @param limit the limit of the line relative to the text
129      * @param dir the paragraph direction of this line
130      * @param directions the directions information of this line
131      * @param hasTabs true if the line might contain tabs or emoji
132      * @param tabStops the tabStops. Can be null.
133      */
set(TextPaint paint, CharSequence text, int start, int limit, int dir, Directions directions, boolean hasTabs, TabStops tabStops)134     void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
135             Directions directions, boolean hasTabs, TabStops tabStops) {
136         mPaint = paint;
137         mText = text;
138         mStart = start;
139         mLen = limit - start;
140         mDir = dir;
141         mDirections = directions;
142         if (mDirections == null) {
143             throw new IllegalArgumentException("Directions cannot be null");
144         }
145         mHasTabs = hasTabs;
146         mSpanned = null;
147 
148         boolean hasReplacement = false;
149         if (text instanceof Spanned) {
150             mSpanned = (Spanned) text;
151             mReplacementSpanSpanSet.init(mSpanned, start, limit);
152             hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
153         }
154 
155         mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT;
156 
157         if (mCharsValid) {
158             if (mChars == null || mChars.length < mLen) {
159                 mChars = ArrayUtils.newUnpaddedCharArray(mLen);
160             }
161             TextUtils.getChars(text, start, limit, mChars, 0);
162             if (hasReplacement) {
163                 // Handle these all at once so we don't have to do it as we go.
164                 // Replace the first character of each replacement run with the
165                 // object-replacement character and the remainder with zero width
166                 // non-break space aka BOM.  Cursor movement code skips these
167                 // zero-width characters.
168                 char[] chars = mChars;
169                 for (int i = start, inext; i < limit; i = inext) {
170                     inext = mReplacementSpanSpanSet.getNextTransition(i, limit);
171                     if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)) {
172                         // transition into a span
173                         chars[i - start] = '\ufffc';
174                         for (int j = i - start + 1, e = inext - start; j < e; ++j) {
175                             chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
176                         }
177                     }
178                 }
179             }
180         }
181         mTabs = tabStops;
182     }
183 
184     /**
185      * Renders the TextLine.
186      *
187      * @param c the canvas to render on
188      * @param x the leading margin position
189      * @param top the top of the line
190      * @param y the baseline
191      * @param bottom the bottom of the line
192      */
draw(Canvas c, float x, int top, int y, int bottom)193     void draw(Canvas c, float x, int top, int y, int bottom) {
194         if (!mHasTabs) {
195             if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
196                 drawRun(c, 0, mLen, false, x, top, y, bottom, false);
197                 return;
198             }
199             if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
200                 drawRun(c, 0, mLen, true, x, top, y, bottom, false);
201                 return;
202             }
203         }
204 
205         float h = 0;
206         int[] runs = mDirections.mDirections;
207         RectF emojiRect = null;
208 
209         int lastRunIndex = runs.length - 2;
210         for (int i = 0; i < runs.length; i += 2) {
211             int runStart = runs[i];
212             int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
213             if (runLimit > mLen) {
214                 runLimit = mLen;
215             }
216             boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
217 
218             int segstart = runStart;
219             for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
220                 int codept = 0;
221                 Bitmap bm = null;
222 
223                 if (mHasTabs && j < runLimit) {
224                     codept = mChars[j];
225                     if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) {
226                         codept = Character.codePointAt(mChars, j);
227                         if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) {
228                             bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
229                         } else if (codept > 0xffff) {
230                             ++j;
231                             continue;
232                         }
233                     }
234                 }
235 
236                 if (j == runLimit || codept == '\t' || bm != null) {
237                     h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom,
238                             i != lastRunIndex || j != mLen);
239 
240                     if (codept == '\t') {
241                         h = mDir * nextTab(h * mDir);
242                     } else if (bm != null) {
243                         float bmAscent = ascent(j);
244                         float bitmapHeight = bm.getHeight();
245                         float scale = -bmAscent / bitmapHeight;
246                         float width = bm.getWidth() * scale;
247 
248                         if (emojiRect == null) {
249                             emojiRect = new RectF();
250                         }
251                         emojiRect.set(x + h, y + bmAscent,
252                                 x + h + width, y);
253                         c.drawBitmap(bm, null, emojiRect, mPaint);
254                         h += width;
255                         j++;
256                     }
257                     segstart = j + 1;
258                 }
259             }
260         }
261     }
262 
263     /**
264      * Returns metrics information for the entire line.
265      *
266      * @param fmi receives font metrics information, can be null
267      * @return the signed width of the line
268      */
metrics(FontMetricsInt fmi)269     float metrics(FontMetricsInt fmi) {
270         return measure(mLen, false, fmi);
271     }
272 
273     /**
274      * Returns information about a position on the line.
275      *
276      * @param offset the line-relative character offset, between 0 and the
277      * line length, inclusive
278      * @param trailing true to measure the trailing edge of the character
279      * before offset, false to measure the leading edge of the character
280      * at offset.
281      * @param fmi receives metrics information about the requested
282      * character, can be null.
283      * @return the signed offset from the leading margin to the requested
284      * character edge.
285      */
measure(int offset, boolean trailing, FontMetricsInt fmi)286     float measure(int offset, boolean trailing, FontMetricsInt fmi) {
287         int target = trailing ? offset - 1 : offset;
288         if (target < 0) {
289             return 0;
290         }
291 
292         float h = 0;
293 
294         if (!mHasTabs) {
295             if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
296                 return measureRun(0, offset, mLen, false, fmi);
297             }
298             if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
299                 return measureRun(0, offset, mLen, true, fmi);
300             }
301         }
302 
303         char[] chars = mChars;
304         int[] runs = mDirections.mDirections;
305         for (int i = 0; i < runs.length; i += 2) {
306             int runStart = runs[i];
307             int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
308             if (runLimit > mLen) {
309                 runLimit = mLen;
310             }
311             boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
312 
313             int segstart = runStart;
314             for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
315                 int codept = 0;
316                 Bitmap bm = null;
317 
318                 if (mHasTabs && j < runLimit) {
319                     codept = chars[j];
320                     if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) {
321                         codept = Character.codePointAt(chars, j);
322                         if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) {
323                             bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
324                         } else if (codept > 0xffff) {
325                             ++j;
326                             continue;
327                         }
328                     }
329                 }
330 
331                 if (j == runLimit || codept == '\t' || bm != null) {
332                     boolean inSegment = target >= segstart && target < j;
333 
334                     boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
335                     if (inSegment && advance) {
336                         return h += measureRun(segstart, offset, j, runIsRtl, fmi);
337                     }
338 
339                     float w = measureRun(segstart, j, j, runIsRtl, fmi);
340                     h += advance ? w : -w;
341 
342                     if (inSegment) {
343                         return h += measureRun(segstart, offset, j, runIsRtl, null);
344                     }
345 
346                     if (codept == '\t') {
347                         if (offset == j) {
348                             return h;
349                         }
350                         h = mDir * nextTab(h * mDir);
351                         if (target == j) {
352                             return h;
353                         }
354                     }
355 
356                     if (bm != null) {
357                         float bmAscent = ascent(j);
358                         float wid = bm.getWidth() * -bmAscent / bm.getHeight();
359                         h += mDir * wid;
360                         j++;
361                     }
362 
363                     segstart = j + 1;
364                 }
365             }
366         }
367 
368         return h;
369     }
370 
371     /**
372      * Draws a unidirectional (but possibly multi-styled) run of text.
373      *
374      *
375      * @param c the canvas to draw on
376      * @param start the line-relative start
377      * @param limit the line-relative limit
378      * @param runIsRtl true if the run is right-to-left
379      * @param x the position of the run that is closest to the leading margin
380      * @param top the top of the line
381      * @param y the baseline
382      * @param bottom the bottom of the line
383      * @param needWidth true if the width value is required.
384      * @return the signed width of the run, based on the paragraph direction.
385      * Only valid if needWidth is true.
386      */
387     private float drawRun(Canvas c, int start,
388             int limit, boolean runIsRtl, float x, int top, int y, int bottom,
389             boolean needWidth) {
390 
391         if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
392             float w = -measureRun(start, limit, limit, runIsRtl, null);
393             handleRun(start, limit, limit, runIsRtl, c, x + w, top,
394                     y, bottom, null, false);
395             return w;
396         }
397 
398         return handleRun(start, limit, limit, runIsRtl, c, x, top,
399                 y, bottom, null, needWidth);
400     }
401 
402     /**
403      * Measures a unidirectional (but possibly multi-styled) run of text.
404      *
405      *
406      * @param start the line-relative start of the run
407      * @param offset the offset to measure to, between start and limit inclusive
408      * @param limit the line-relative limit of the run
409      * @param runIsRtl true if the run is right-to-left
410      * @param fmi receives metrics information about the requested
411      * run, can be null.
412      * @return the signed width from the start of the run to the leading edge
413      * of the character at offset, based on the run (not paragraph) direction
414      */
415     private float measureRun(int start, int offset, int limit, boolean runIsRtl,
416             FontMetricsInt fmi) {
417         return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true);
418     }
419 
420     /**
421      * Walk the cursor through this line, skipping conjuncts and
422      * zero-width characters.
423      *
424      * <p>This function cannot properly walk the cursor off the ends of the line
425      * since it does not know about any shaping on the previous/following line
426      * that might affect the cursor position. Callers must either avoid these
427      * situations or handle the result specially.
428      *
429      * @param cursor the starting position of the cursor, between 0 and the
430      * length of the line, inclusive
431      * @param toLeft true if the caret is moving to the left.
432      * @return the new offset.  If it is less than 0 or greater than the length
433      * of the line, the previous/following line should be examined to get the
434      * actual offset.
435      */
436     int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
437         // 1) The caret marks the leading edge of a character. The character
438         // logically before it might be on a different level, and the active caret
439         // position is on the character at the lower level. If that character
440         // was the previous character, the caret is on its trailing edge.
441         // 2) Take this character/edge and move it in the indicated direction.
442         // This gives you a new character and a new edge.
443         // 3) This position is between two visually adjacent characters.  One of
444         // these might be at a lower level.  The active position is on the
445         // character at the lower level.
446         // 4) If the active position is on the trailing edge of the character,
447         // the new caret position is the following logical character, else it
448         // is the character.
449 
450         int lineStart = 0;
451         int lineEnd = mLen;
452         boolean paraIsRtl = mDir == -1;
453         int[] runs = mDirections.mDirections;
454 
455         int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
456         boolean trailing = false;
457 
458         if (cursor == lineStart) {
459             runIndex = -2;
460         } else if (cursor == lineEnd) {
461             runIndex = runs.length;
462         } else {
463           // First, get information about the run containing the character with
464           // the active caret.
465           for (runIndex = 0; runIndex < runs.length; runIndex += 2) {
466             runStart = lineStart + runs[runIndex];
467             if (cursor >= runStart) {
468               runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK);
469               if (runLimit > lineEnd) {
470                   runLimit = lineEnd;
471               }
472               if (cursor < runLimit) {
473                 runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
474                     Layout.RUN_LEVEL_MASK;
475                 if (cursor == runStart) {
476                   // The caret is on a run boundary, see if we should
477                   // use the position on the trailing edge of the previous
478                   // logical character instead.
479                   int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;
480                   int pos = cursor - 1;
481                   for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {
482                     prevRunStart = lineStart + runs[prevRunIndex];
483                     if (pos >= prevRunStart) {
484                       prevRunLimit = prevRunStart +
485                           (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK);
486                       if (prevRunLimit > lineEnd) {
487                           prevRunLimit = lineEnd;
488                       }
489                       if (pos < prevRunLimit) {
490                         prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT)
491                             & Layout.RUN_LEVEL_MASK;
492                         if (prevRunLevel < runLevel) {
493                           // Start from logically previous character.
494                           runIndex = prevRunIndex;
495                           runLevel = prevRunLevel;
496                           runStart = prevRunStart;
497                           runLimit = prevRunLimit;
498                           trailing = true;
499                           break;
500                         }
501                       }
502                     }
503                   }
504                 }
505                 break;
506               }
507             }
508           }
509 
510           // caret might be == lineEnd.  This is generally a space or paragraph
511           // separator and has an associated run, but might be the end of
512           // text, in which case it doesn't.  If that happens, we ran off the
513           // end of the run list, and runIndex == runs.length.  In this case,
514           // we are at a run boundary so we skip the below test.
515           if (runIndex != runs.length) {
516               boolean runIsRtl = (runLevel & 0x1) != 0;
517               boolean advance = toLeft == runIsRtl;
518               if (cursor != (advance ? runLimit : runStart) || advance != trailing) {
519                   // Moving within or into the run, so we can move logically.
520                   newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit,
521                           runIsRtl, cursor, advance);
522                   // If the new position is internal to the run, we're at the strong
523                   // position already so we're finished.
524                   if (newCaret != (advance ? runLimit : runStart)) {
525                       return newCaret;
526                   }
527               }
528           }
529         }
530 
531         // If newCaret is -1, we're starting at a run boundary and crossing
532         // into another run. Otherwise we've arrived at a run boundary, and
533         // need to figure out which character to attach to.  Note we might
534         // need to run this twice, if we cross a run boundary and end up at
535         // another run boundary.
536         while (true) {
537           boolean advance = toLeft == paraIsRtl;
538           int otherRunIndex = runIndex + (advance ? 2 : -2);
539           if (otherRunIndex >= 0 && otherRunIndex < runs.length) {
540             int otherRunStart = lineStart + runs[otherRunIndex];
541             int otherRunLimit = otherRunStart +
542             (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK);
543             if (otherRunLimit > lineEnd) {
544                 otherRunLimit = lineEnd;
545             }
546             int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
547                 Layout.RUN_LEVEL_MASK;
548             boolean otherRunIsRtl = (otherRunLevel & 1) != 0;
549 
550             advance = toLeft == otherRunIsRtl;
551             if (newCaret == -1) {
552                 newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart,
553                         otherRunLimit, otherRunIsRtl,
554                         advance ? otherRunStart : otherRunLimit, advance);
555                 if (newCaret == (advance ? otherRunLimit : otherRunStart)) {
556                     // Crossed and ended up at a new boundary,
557                     // repeat a second and final time.
558                     runIndex = otherRunIndex;
559                     runLevel = otherRunLevel;
560                     continue;
561                 }
562                 break;
563             }
564 
565             // The new caret is at a boundary.
566             if (otherRunLevel < runLevel) {
567               // The strong character is in the other run.
568               newCaret = advance ? otherRunStart : otherRunLimit;
569             }
570             break;
571           }
572 
573           if (newCaret == -1) {
574               // We're walking off the end of the line.  The paragraph
575               // level is always equal to or lower than any internal level, so
576               // the boundaries get the strong caret.
577               newCaret = advance ? mLen + 1 : -1;
578               break;
579           }
580 
581           // Else we've arrived at the end of the line.  That's a strong position.
582           // We might have arrived here by crossing over a run with no internal
583           // breaks and dropping out of the above loop before advancing one final
584           // time, so reset the caret.
585           // Note, we use '<=' below to handle a situation where the only run
586           // on the line is a counter-directional run.  If we're not advancing,
587           // we can end up at the 'lineEnd' position but the caret we want is at
588           // the lineStart.
589           if (newCaret <= lineEnd) {
590               newCaret = advance ? lineEnd : lineStart;
591           }
592           break;
593         }
594 
595         return newCaret;
596     }
597 
598     /**
599      * Returns the next valid offset within this directional run, skipping
600      * conjuncts and zero-width characters.  This should not be called to walk
601      * off the end of the line, since the returned values might not be valid
602      * on neighboring lines.  If the returned offset is less than zero or
603      * greater than the line length, the offset should be recomputed on the
604      * preceding or following line, respectively.
605      *
606      * @param runIndex the run index
607      * @param runStart the start of the run
608      * @param runLimit the limit of the run
609      * @param runIsRtl true if the run is right-to-left
610      * @param offset the offset
611      * @param after true if the new offset should logically follow the provided
612      * offset
613      * @return the new offset
614      */
getOffsetBeforeAfter(int runIndex, int runStart, int runLimit, boolean runIsRtl, int offset, boolean after)615     private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit,
616             boolean runIsRtl, int offset, boolean after) {
617 
618         if (runIndex < 0 || offset == (after ? mLen : 0)) {
619             // Walking off end of line.  Since we don't know
620             // what cursor positions are available on other lines, we can't
621             // return accurate values.  These are a guess.
622             if (after) {
623                 return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;
624             }
625             return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;
626         }
627 
628         TextPaint wp = mWorkPaint;
629         wp.set(mPaint);
630 
631         int spanStart = runStart;
632         int spanLimit;
633         if (mSpanned == null) {
634             spanLimit = runLimit;
635         } else {
636             int target = after ? offset + 1 : offset;
637             int limit = mStart + runLimit;
638             while (true) {
639                 spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,
640                         MetricAffectingSpan.class) - mStart;
641                 if (spanLimit >= target) {
642                     break;
643                 }
644                 spanStart = spanLimit;
645             }
646 
647             MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,
648                     mStart + spanLimit, MetricAffectingSpan.class);
649             spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);
650 
651             if (spans.length > 0) {
652                 ReplacementSpan replacement = null;
653                 for (int j = 0; j < spans.length; j++) {
654                     MetricAffectingSpan span = spans[j];
655                     if (span instanceof ReplacementSpan) {
656                         replacement = (ReplacementSpan)span;
657                     } else {
658                         span.updateMeasureState(wp);
659                     }
660                 }
661 
662                 if (replacement != null) {
663                     // If we have a replacement span, we're moving either to
664                     // the start or end of this span.
665                     return after ? spanLimit : spanStart;
666                 }
667             }
668         }
669 
670         int dir = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
671         int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;
672         if (mCharsValid) {
673             return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,
674                     dir, offset, cursorOpt);
675         } else {
676             return wp.getTextRunCursor(mText, mStart + spanStart,
677                     mStart + spanLimit, dir, mStart + offset, cursorOpt) - mStart;
678         }
679     }
680 
681     /**
682      * @param wp
683      */
expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp)684     private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
685         final int previousTop     = fmi.top;
686         final int previousAscent  = fmi.ascent;
687         final int previousDescent = fmi.descent;
688         final int previousBottom  = fmi.bottom;
689         final int previousLeading = fmi.leading;
690 
691         wp.getFontMetricsInt(fmi);
692 
693         updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
694                 previousLeading);
695     }
696 
updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent, int previousDescent, int previousBottom, int previousLeading)697     static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
698             int previousDescent, int previousBottom, int previousLeading) {
699         fmi.top     = Math.min(fmi.top,     previousTop);
700         fmi.ascent  = Math.min(fmi.ascent,  previousAscent);
701         fmi.descent = Math.max(fmi.descent, previousDescent);
702         fmi.bottom  = Math.max(fmi.bottom,  previousBottom);
703         fmi.leading = Math.max(fmi.leading, previousLeading);
704     }
705 
706     /**
707      * Utility function for measuring and rendering text.  The text must
708      * not include a tab or emoji.
709      *
710      * @param wp the working paint
711      * @param start the start of the text
712      * @param end the end of the text
713      * @param runIsRtl true if the run is right-to-left
714      * @param c the canvas, can be null if rendering is not needed
715      * @param x the edge of the run closest to the leading margin
716      * @param top the top of the line
717      * @param y the baseline
718      * @param bottom the bottom of the line
719      * @param fmi receives metrics information, can be null
720      * @param needWidth true if the width of the run is needed
721      * @return the signed width of the run based on the run direction; only
722      * valid if needWidth is true
723      */
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)724     private float handleText(TextPaint wp, int start, int end,
725             int contextStart, int contextEnd, boolean runIsRtl,
726             Canvas c, float x, int top, int y, int bottom,
727             FontMetricsInt fmi, boolean needWidth) {
728 
729         // Get metrics first (even for empty strings or "0" width runs)
730         if (fmi != null) {
731             expandMetricsFromPaint(fmi, wp);
732         }
733 
734         int runLen = end - start;
735         // No need to do anything if the run width is "0"
736         if (runLen == 0) {
737             return 0f;
738         }
739 
740         float ret = 0;
741 
742         if (needWidth || (c != null && (wp.bgColor != 0 || wp.underlineColor != 0 || runIsRtl))) {
743             if (mCharsValid) {
744                 ret = wp.getRunAdvance(mChars, start, end, contextStart, contextEnd,
745                         runIsRtl, end);
746             } else {
747                 int delta = mStart;
748                 ret = wp.getRunAdvance(mText, delta + start, delta + end,
749                         delta + contextStart, delta + contextEnd, runIsRtl, delta + end);
750             }
751         }
752 
753         if (c != null) {
754             if (runIsRtl) {
755                 x -= ret;
756             }
757 
758             if (wp.bgColor != 0) {
759                 int previousColor = wp.getColor();
760                 Paint.Style previousStyle = wp.getStyle();
761 
762                 wp.setColor(wp.bgColor);
763                 wp.setStyle(Paint.Style.FILL);
764                 c.drawRect(x, top, x + ret, bottom, wp);
765 
766                 wp.setStyle(previousStyle);
767                 wp.setColor(previousColor);
768             }
769 
770             if (wp.underlineColor != 0) {
771                 // kStdUnderline_Offset = 1/9, defined in SkTextFormatParams.h
772                 float underlineTop = y + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize();
773 
774                 int previousColor = wp.getColor();
775                 Paint.Style previousStyle = wp.getStyle();
776                 boolean previousAntiAlias = wp.isAntiAlias();
777 
778                 wp.setStyle(Paint.Style.FILL);
779                 wp.setAntiAlias(true);
780 
781                 wp.setColor(wp.underlineColor);
782                 c.drawRect(x, underlineTop, x + ret, underlineTop + wp.underlineThickness, wp);
783 
784                 wp.setStyle(previousStyle);
785                 wp.setColor(previousColor);
786                 wp.setAntiAlias(previousAntiAlias);
787             }
788 
789             drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
790                     x, y + wp.baselineShift);
791         }
792 
793         return runIsRtl ? -ret : ret;
794     }
795 
796     /**
797      * Utility function for measuring and rendering a replacement.
798      *
799      *
800      * @param replacement the replacement
801      * @param wp the work paint
802      * @param start the start of the run
803      * @param limit the limit of the run
804      * @param runIsRtl true if the run is right-to-left
805      * @param c the canvas, can be null if not rendering
806      * @param x the edge of the replacement closest to the leading margin
807      * @param top the top of the line
808      * @param y the baseline
809      * @param bottom the bottom of the line
810      * @param fmi receives metrics information, can be null
811      * @param needWidth true if the width of the replacement is needed
812      * @return the signed width of the run based on the run direction; only
813      * valid if needWidth is true
814      */
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)815     private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
816             int start, int limit, boolean runIsRtl, Canvas c,
817             float x, int top, int y, int bottom, FontMetricsInt fmi,
818             boolean needWidth) {
819 
820         float ret = 0;
821 
822         int textStart = mStart + start;
823         int textLimit = mStart + limit;
824 
825         if (needWidth || (c != null && runIsRtl)) {
826             int previousTop = 0;
827             int previousAscent = 0;
828             int previousDescent = 0;
829             int previousBottom = 0;
830             int previousLeading = 0;
831 
832             boolean needUpdateMetrics = (fmi != null);
833 
834             if (needUpdateMetrics) {
835                 previousTop     = fmi.top;
836                 previousAscent  = fmi.ascent;
837                 previousDescent = fmi.descent;
838                 previousBottom  = fmi.bottom;
839                 previousLeading = fmi.leading;
840             }
841 
842             ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
843 
844             if (needUpdateMetrics) {
845                 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
846                         previousLeading);
847             }
848         }
849 
850         if (c != null) {
851             if (runIsRtl) {
852                 x -= ret;
853             }
854             replacement.draw(c, mText, textStart, textLimit,
855                     x, top, y, bottom, wp);
856         }
857 
858         return runIsRtl ? -ret : ret;
859     }
860 
861     /**
862      * Utility function for handling a unidirectional run.  The run must not
863      * contain tabs or emoji but can contain styles.
864      *
865      *
866      * @param start the line-relative start of the run
867      * @param measureLimit the offset to measure to, between start and limit inclusive
868      * @param limit the limit of the run
869      * @param runIsRtl true if the run is right-to-left
870      * @param c the canvas, can be null
871      * @param x the end of the run closest to the leading margin
872      * @param top the top of the line
873      * @param y the baseline
874      * @param bottom the bottom of the line
875      * @param fmi receives metrics information, can be null
876      * @param needWidth true if the width is required
877      * @return the signed width of the run based on the run direction; only
878      * valid if needWidth is true
879      */
handleRun(int start, int measureLimit, int limit, boolean runIsRtl, Canvas c, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth)880     private float handleRun(int start, int measureLimit,
881             int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
882             int bottom, FontMetricsInt fmi, boolean needWidth) {
883 
884         // Case of an empty line, make sure we update fmi according to mPaint
885         if (start == measureLimit) {
886             TextPaint wp = mWorkPaint;
887             wp.set(mPaint);
888             if (fmi != null) {
889                 expandMetricsFromPaint(fmi, wp);
890             }
891             return 0f;
892         }
893 
894         if (mSpanned == null) {
895             TextPaint wp = mWorkPaint;
896             wp.set(mPaint);
897             final int mlimit = measureLimit;
898             return handleText(wp, start, mlimit, start, limit, runIsRtl, c, x, top,
899                     y, bottom, fmi, needWidth || mlimit < measureLimit);
900         }
901 
902         mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
903         mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
904 
905         // Shaping needs to take into account context up to metric boundaries,
906         // but rendering needs to take into account character style boundaries.
907         // So we iterate through metric runs to get metric bounds,
908         // then within each metric run iterate through character style runs
909         // for the run bounds.
910         final float originalX = x;
911         for (int i = start, inext; i < measureLimit; i = inext) {
912             TextPaint wp = mWorkPaint;
913             wp.set(mPaint);
914 
915             inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
916                     mStart;
917             int mlimit = Math.min(inext, measureLimit);
918 
919             ReplacementSpan replacement = null;
920 
921             for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
922                 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
923                 // empty by construction. This special case in getSpans() explains the >= & <= tests
924                 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||
925                         (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
926                 MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
927                 if (span instanceof ReplacementSpan) {
928                     replacement = (ReplacementSpan)span;
929                 } else {
930                     // We might have a replacement that uses the draw
931                     // state, otherwise measure state would suffice.
932                     span.updateDrawState(wp);
933                 }
934             }
935 
936             if (replacement != null) {
937                 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
938                         bottom, fmi, needWidth || mlimit < measureLimit);
939                 continue;
940             }
941 
942             for (int j = i, jnext; j < mlimit; j = jnext) {
943                 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + mlimit) -
944                         mStart;
945 
946                 wp.set(mPaint);
947                 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
948                     // Intentionally using >= and <= as explained above
949                     if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + jnext) ||
950                             (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
951 
952                     CharacterStyle span = mCharacterStyleSpanSet.spans[k];
953                     span.updateDrawState(wp);
954                 }
955 
956                 // Only draw hyphen on last run in line
957                 if (jnext < mLen) {
958                     wp.setHyphenEdit(0);
959                 }
960                 x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,
961                         top, y, bottom, fmi, needWidth || jnext < measureLimit);
962             }
963         }
964 
965         return x - originalX;
966     }
967 
968     /**
969      * Render a text run with the set-up paint.
970      *
971      * @param c the canvas
972      * @param wp the paint used to render the text
973      * @param start the start of the run
974      * @param end the end of the run
975      * @param contextStart the start of context for the run
976      * @param contextEnd the end of the context for the run
977      * @param runIsRtl true if the run is right-to-left
978      * @param x the x position of the left edge of the run
979      * @param y the baseline of the run
980      */
drawTextRun(Canvas c, TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x, int y)981     private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
982             int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
983 
984         if (mCharsValid) {
985             int count = end - start;
986             int contextCount = contextEnd - contextStart;
987             c.drawTextRun(mChars, start, count, contextStart, contextCount,
988                     x, y, runIsRtl, wp);
989         } else {
990             int delta = mStart;
991             c.drawTextRun(mText, delta + start, delta + end,
992                     delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
993         }
994     }
995 
996     /**
997      * Returns the ascent of the text at start.  This is used for scaling
998      * emoji.
999      *
1000      * @param pos the line-relative position
1001      * @return the ascent of the text at start
1002      */
ascent(int pos)1003     float ascent(int pos) {
1004         if (mSpanned == null) {
1005             return mPaint.ascent();
1006         }
1007 
1008         pos += mStart;
1009         MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1, MetricAffectingSpan.class);
1010         if (spans.length == 0) {
1011             return mPaint.ascent();
1012         }
1013 
1014         TextPaint wp = mWorkPaint;
1015         wp.set(mPaint);
1016         for (MetricAffectingSpan span : spans) {
1017             span.updateMeasureState(wp);
1018         }
1019         return wp.ascent();
1020     }
1021 
1022     /**
1023      * Returns the next tab position.
1024      *
1025      * @param h the (unsigned) offset from the leading margin
1026      * @return the (unsigned) tab position after this offset
1027      */
nextTab(float h)1028     float nextTab(float h) {
1029         if (mTabs != null) {
1030             return mTabs.nextTab(h);
1031         }
1032         return TabStops.nextDefaultStop(h, TAB_INCREMENT);
1033     }
1034 
1035     private static final int TAB_INCREMENT = 20;
1036 }
1037