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