• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.text;
18 
19 import android.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         int contextLen = contextEnd - contextStart;
743         if (needWidth || (c != null && (wp.bgColor != 0 || wp.underlineColor != 0 || runIsRtl))) {
744             if (mCharsValid) {
745                 ret = wp.getTextRunAdvances(mChars, start, runLen,
746                         contextStart, contextLen, runIsRtl, null, 0);
747             } else {
748                 int delta = mStart;
749                 ret = wp.getTextRunAdvances(mText, delta + start,
750                         delta + end, delta + contextStart, delta + contextEnd,
751                         runIsRtl, null, 0);
752             }
753         }
754 
755         if (c != null) {
756             if (runIsRtl) {
757                 x -= ret;
758             }
759 
760             if (wp.bgColor != 0) {
761                 int previousColor = wp.getColor();
762                 Paint.Style previousStyle = wp.getStyle();
763 
764                 wp.setColor(wp.bgColor);
765                 wp.setStyle(Paint.Style.FILL);
766                 c.drawRect(x, top, x + ret, bottom, wp);
767 
768                 wp.setStyle(previousStyle);
769                 wp.setColor(previousColor);
770             }
771 
772             if (wp.underlineColor != 0) {
773                 // kStdUnderline_Offset = 1/9, defined in SkTextFormatParams.h
774                 float underlineTop = y + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize();
775 
776                 int previousColor = wp.getColor();
777                 Paint.Style previousStyle = wp.getStyle();
778                 boolean previousAntiAlias = wp.isAntiAlias();
779 
780                 wp.setStyle(Paint.Style.FILL);
781                 wp.setAntiAlias(true);
782 
783                 wp.setColor(wp.underlineColor);
784                 c.drawRect(x, underlineTop, x + ret, underlineTop + wp.underlineThickness, wp);
785 
786                 wp.setStyle(previousStyle);
787                 wp.setColor(previousColor);
788                 wp.setAntiAlias(previousAntiAlias);
789             }
790 
791             drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
792                     x, y + wp.baselineShift);
793         }
794 
795         return runIsRtl ? -ret : ret;
796     }
797 
798     /**
799      * Utility function for measuring and rendering a replacement.
800      *
801      *
802      * @param replacement the replacement
803      * @param wp the work paint
804      * @param start the start of the run
805      * @param limit the limit of the run
806      * @param runIsRtl true if the run is right-to-left
807      * @param c the canvas, can be null if not rendering
808      * @param x the edge of the replacement closest to the leading margin
809      * @param top the top of the line
810      * @param y the baseline
811      * @param bottom the bottom of the line
812      * @param fmi receives metrics information, can be null
813      * @param needWidth true if the width of the replacement is needed
814      * @return the signed width of the run based on the run direction; only
815      * valid if needWidth is true
816      */
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)817     private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
818             int start, int limit, boolean runIsRtl, Canvas c,
819             float x, int top, int y, int bottom, FontMetricsInt fmi,
820             boolean needWidth) {
821 
822         float ret = 0;
823 
824         int textStart = mStart + start;
825         int textLimit = mStart + limit;
826 
827         if (needWidth || (c != null && runIsRtl)) {
828             int previousTop = 0;
829             int previousAscent = 0;
830             int previousDescent = 0;
831             int previousBottom = 0;
832             int previousLeading = 0;
833 
834             boolean needUpdateMetrics = (fmi != null);
835 
836             if (needUpdateMetrics) {
837                 previousTop     = fmi.top;
838                 previousAscent  = fmi.ascent;
839                 previousDescent = fmi.descent;
840                 previousBottom  = fmi.bottom;
841                 previousLeading = fmi.leading;
842             }
843 
844             ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
845 
846             if (needUpdateMetrics) {
847                 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
848                         previousLeading);
849             }
850         }
851 
852         if (c != null) {
853             if (runIsRtl) {
854                 x -= ret;
855             }
856             replacement.draw(c, mText, textStart, textLimit,
857                     x, top, y, bottom, wp);
858         }
859 
860         return runIsRtl ? -ret : ret;
861     }
862 
863     /**
864      * Utility function for handling a unidirectional run.  The run must not
865      * contain tabs or emoji but can contain styles.
866      *
867      *
868      * @param start the line-relative start of the run
869      * @param measureLimit the offset to measure to, between start and limit inclusive
870      * @param limit the limit of the run
871      * @param runIsRtl true if the run is right-to-left
872      * @param c the canvas, can be null
873      * @param x the end of the run closest to the leading margin
874      * @param top the top of the line
875      * @param y the baseline
876      * @param bottom the bottom of the line
877      * @param fmi receives metrics information, can be null
878      * @param needWidth true if the width is required
879      * @return the signed width of the run based on the run direction; only
880      * valid if needWidth is true
881      */
handleRun(int start, int measureLimit, int limit, boolean runIsRtl, Canvas c, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth)882     private float handleRun(int start, int measureLimit,
883             int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
884             int bottom, FontMetricsInt fmi, boolean needWidth) {
885 
886         // Case of an empty line, make sure we update fmi according to mPaint
887         if (start == measureLimit) {
888             TextPaint wp = mWorkPaint;
889             wp.set(mPaint);
890             if (fmi != null) {
891                 expandMetricsFromPaint(fmi, wp);
892             }
893             return 0f;
894         }
895 
896         if (mSpanned == null) {
897             TextPaint wp = mWorkPaint;
898             wp.set(mPaint);
899             final int mlimit = measureLimit;
900             return handleText(wp, start, mlimit, start, limit, runIsRtl, c, x, top,
901                     y, bottom, fmi, needWidth || mlimit < measureLimit);
902         }
903 
904         mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
905         mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
906 
907         // Shaping needs to take into account context up to metric boundaries,
908         // but rendering needs to take into account character style boundaries.
909         // So we iterate through metric runs to get metric bounds,
910         // then within each metric run iterate through character style runs
911         // for the run bounds.
912         final float originalX = x;
913         for (int i = start, inext; i < measureLimit; i = inext) {
914             TextPaint wp = mWorkPaint;
915             wp.set(mPaint);
916 
917             inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
918                     mStart;
919             int mlimit = Math.min(inext, measureLimit);
920 
921             ReplacementSpan replacement = null;
922 
923             for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
924                 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
925                 // empty by construction. This special case in getSpans() explains the >= & <= tests
926                 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||
927                         (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
928                 MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
929                 if (span instanceof ReplacementSpan) {
930                     replacement = (ReplacementSpan)span;
931                 } else {
932                     // We might have a replacement that uses the draw
933                     // state, otherwise measure state would suffice.
934                     span.updateDrawState(wp);
935                 }
936             }
937 
938             if (replacement != null) {
939                 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
940                         bottom, fmi, needWidth || mlimit < measureLimit);
941                 continue;
942             }
943 
944             for (int j = i, jnext; j < mlimit; j = jnext) {
945                 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + mlimit) -
946                         mStart;
947 
948                 wp.set(mPaint);
949                 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
950                     // Intentionally using >= and <= as explained above
951                     if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + jnext) ||
952                             (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
953 
954                     CharacterStyle span = mCharacterStyleSpanSet.spans[k];
955                     span.updateDrawState(wp);
956                 }
957 
958                 x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,
959                         top, y, bottom, fmi, needWidth || jnext < measureLimit);
960             }
961         }
962 
963         return x - originalX;
964     }
965 
966     /**
967      * Render a text run with the set-up paint.
968      *
969      * @param c the canvas
970      * @param wp the paint used to render the text
971      * @param start the start of the run
972      * @param end the end of the run
973      * @param contextStart the start of context for the run
974      * @param contextEnd the end of the context for the run
975      * @param runIsRtl true if the run is right-to-left
976      * @param x the x position of the left edge of the run
977      * @param y the baseline of the run
978      */
drawTextRun(Canvas c, TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x, int y)979     private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
980             int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
981 
982         if (mCharsValid) {
983             int count = end - start;
984             int contextCount = contextEnd - contextStart;
985             c.drawTextRun(mChars, start, count, contextStart, contextCount,
986                     x, y, runIsRtl, wp);
987         } else {
988             int delta = mStart;
989             c.drawTextRun(mText, delta + start, delta + end,
990                     delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
991         }
992     }
993 
994     /**
995      * Returns the ascent of the text at start.  This is used for scaling
996      * emoji.
997      *
998      * @param pos the line-relative position
999      * @return the ascent of the text at start
1000      */
ascent(int pos)1001     float ascent(int pos) {
1002         if (mSpanned == null) {
1003             return mPaint.ascent();
1004         }
1005 
1006         pos += mStart;
1007         MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1, MetricAffectingSpan.class);
1008         if (spans.length == 0) {
1009             return mPaint.ascent();
1010         }
1011 
1012         TextPaint wp = mWorkPaint;
1013         wp.set(mPaint);
1014         for (MetricAffectingSpan span : spans) {
1015             span.updateMeasureState(wp);
1016         }
1017         return wp.ascent();
1018     }
1019 
1020     /**
1021      * Returns the next tab position.
1022      *
1023      * @param h the (unsigned) offset from the leading margin
1024      * @return the (unsigned) tab position after this offset
1025      */
nextTab(float h)1026     float nextTab(float h) {
1027         if (mTabs != null) {
1028             return mTabs.nextTab(h);
1029         }
1030         return TabStops.nextDefaultStop(h, TAB_INCREMENT);
1031     }
1032 
1033     private static final int TAB_INCREMENT = 20;
1034 }
1035