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