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