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