TextLine.java revision 554585e08da5e89762105b2adc0b4c76651d1d68
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                        final float thickness =
831                                Math.max(((Paint) wp).getUnderlineThickness(), 1.0f);
832                        drawUnderline(wp, c, wp.getColor(), thickness,
833                                underlineXLeft, underlineXRight, y);
834                    }
835                }
836            }
837
838            drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
839                    leftX, y + wp.baselineShift);
840        }
841
842        return runIsRtl ? -totalWidth : totalWidth;
843    }
844
845    /**
846     * Utility function for measuring and rendering a replacement.
847     *
848     *
849     * @param replacement the replacement
850     * @param wp the work paint
851     * @param start the start of the run
852     * @param limit the limit of the run
853     * @param runIsRtl true if the run is right-to-left
854     * @param c the canvas, can be null if not rendering
855     * @param x the edge of the replacement closest to the leading margin
856     * @param top the top of the line
857     * @param y the baseline
858     * @param bottom the bottom of the line
859     * @param fmi receives metrics information, can be null
860     * @param needWidth true if the width of the replacement is needed
861     * @return the signed width of the run based on the run direction; only
862     * valid if needWidth is true
863     */
864    private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
865            int start, int limit, boolean runIsRtl, Canvas c,
866            float x, int top, int y, int bottom, FontMetricsInt fmi,
867            boolean needWidth) {
868
869        float ret = 0;
870
871        int textStart = mStart + start;
872        int textLimit = mStart + limit;
873
874        if (needWidth || (c != null && runIsRtl)) {
875            int previousTop = 0;
876            int previousAscent = 0;
877            int previousDescent = 0;
878            int previousBottom = 0;
879            int previousLeading = 0;
880
881            boolean needUpdateMetrics = (fmi != null);
882
883            if (needUpdateMetrics) {
884                previousTop     = fmi.top;
885                previousAscent  = fmi.ascent;
886                previousDescent = fmi.descent;
887                previousBottom  = fmi.bottom;
888                previousLeading = fmi.leading;
889            }
890
891            ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
892
893            if (needUpdateMetrics) {
894                updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
895                        previousLeading);
896            }
897        }
898
899        if (c != null) {
900            if (runIsRtl) {
901                x -= ret;
902            }
903            replacement.draw(c, mText, textStart, textLimit,
904                    x, top, y, bottom, wp);
905        }
906
907        return runIsRtl ? -ret : ret;
908    }
909
910    private int adjustHyphenEdit(int start, int limit, int hyphenEdit) {
911        int result = hyphenEdit;
912        // Only draw hyphens on first or last run in line. Disable them otherwise.
913        if (start > 0) { // not the first run
914            result &= ~Paint.HYPHENEDIT_MASK_START_OF_LINE;
915        }
916        if (limit < mLen) { // not the last run
917            result &= ~Paint.HYPHENEDIT_MASK_END_OF_LINE;
918        }
919        return result;
920    }
921
922    private static final class UnderlineInfo {
923        public boolean isUnderlineText;
924        public int underlineColor;
925        public float underlineThickness;
926        public int start = -1;
927        public int end = -1;
928
929        public boolean hasUnderline() {
930            return isUnderlineText || underlineColor != 0;
931        }
932
933        // Copies the info, but not the start and end range.
934        public UnderlineInfo copyInfo() {
935            final UnderlineInfo copy = new UnderlineInfo();
936            copy.isUnderlineText = isUnderlineText;
937            copy.underlineColor = underlineColor;
938            copy.underlineThickness = underlineThickness;
939            return copy;
940        }
941    }
942
943    private void extractUnderlineInfo(@NonNull TextPaint paint, @NonNull UnderlineInfo info) {
944        info.isUnderlineText = paint.isUnderlineText();
945        if (info.isUnderlineText) {
946            paint.setUnderlineText(false);
947        }
948        info.underlineColor = paint.underlineColor;
949        info.underlineThickness = paint.underlineThickness;
950        paint.setUnderlineText(0, 0.0f);
951    }
952
953    /**
954     * Utility function for handling a unidirectional run.  The run must not
955     * contain tabs but can contain styles.
956     *
957     *
958     * @param start the line-relative start of the run
959     * @param measureLimit the offset to measure to, between start and limit inclusive
960     * @param limit the limit of the run
961     * @param runIsRtl true if the run is right-to-left
962     * @param c the canvas, can be null
963     * @param x the end of the run closest to the leading margin
964     * @param top the top of the line
965     * @param y the baseline
966     * @param bottom the bottom of the line
967     * @param fmi receives metrics information, can be null
968     * @param needWidth true if the width is required
969     * @return the signed width of the run based on the run direction; only
970     * valid if needWidth is true
971     */
972    private float handleRun(int start, int measureLimit,
973            int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
974            int bottom, FontMetricsInt fmi, boolean needWidth) {
975
976        if (measureLimit < start || measureLimit > limit) {
977            throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
978                    + "start (" + start + ") and limit (" + limit + ") bounds");
979        }
980
981        // Case of an empty line, make sure we update fmi according to mPaint
982        if (start == measureLimit) {
983            final TextPaint wp = mWorkPaint;
984            wp.set(mPaint);
985            if (fmi != null) {
986                expandMetricsFromPaint(fmi, wp);
987            }
988            return 0f;
989        }
990
991        final boolean needsSpanMeasurement;
992        if (mSpanned == null) {
993            needsSpanMeasurement = false;
994        } else {
995            mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
996            mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
997            needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0
998                    || mCharacterStyleSpanSet.numberOfSpans != 0;
999        }
1000
1001        if (!needsSpanMeasurement) {
1002            final TextPaint wp = mWorkPaint;
1003            wp.set(mPaint);
1004            wp.setHyphenEdit(adjustHyphenEdit(start, limit, wp.getHyphenEdit()));
1005            return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,
1006                    y, bottom, fmi, needWidth, measureLimit, null);
1007        }
1008
1009        // Shaping needs to take into account context up to metric boundaries,
1010        // but rendering needs to take into account character style boundaries.
1011        // So we iterate through metric runs to get metric bounds,
1012        // then within each metric run iterate through character style runs
1013        // for the run bounds.
1014        final float originalX = x;
1015        for (int i = start, inext; i < measureLimit; i = inext) {
1016            final TextPaint wp = mWorkPaint;
1017            wp.set(mPaint);
1018
1019            inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
1020                    mStart;
1021            int mlimit = Math.min(inext, measureLimit);
1022
1023            ReplacementSpan replacement = null;
1024
1025            for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
1026                // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
1027                // empty by construction. This special case in getSpans() explains the >= & <= tests
1028                if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||
1029                        (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
1030                final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
1031                if (span instanceof ReplacementSpan) {
1032                    replacement = (ReplacementSpan)span;
1033                } else {
1034                    // We might have a replacement that uses the draw
1035                    // state, otherwise measure state would suffice.
1036                    span.updateDrawState(wp);
1037                }
1038            }
1039
1040            if (replacement != null) {
1041                x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
1042                        bottom, fmi, needWidth || mlimit < measureLimit);
1043                continue;
1044            }
1045
1046            final TextPaint activePaint = mActivePaint;
1047            activePaint.set(mPaint);
1048            int activeStart = i;
1049            int activeEnd = mlimit;
1050            final UnderlineInfo underlineInfo = mUnderlineInfo;
1051            mUnderlines.clear();
1052            for (int j = i, jnext; j < mlimit; j = jnext) {
1053                jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
1054                        mStart;
1055
1056                final int offset = Math.min(jnext, mlimit);
1057                wp.set(mPaint);
1058                for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
1059                    // Intentionally using >= and <= as explained above
1060                    if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
1061                            (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
1062
1063                    final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
1064                    span.updateDrawState(wp);
1065                }
1066
1067                extractUnderlineInfo(wp, underlineInfo);
1068
1069                if (j == i) {
1070                    // First chunk of text. We can't handle it yet, since we may need to merge it
1071                    // with the next chunk. So we just save the TextPaint for future comparisons
1072                    // and use.
1073                    activePaint.set(wp);
1074                } else if (!wp.hasEqualAttributes(activePaint)) {
1075                    // The style of the present chunk of text is substantially different from the
1076                    // style of the previous chunk. We need to handle the active piece of text
1077                    // and restart with the present chunk.
1078                    activePaint.setHyphenEdit(adjustHyphenEdit(
1079                            activeStart, activeEnd, mPaint.getHyphenEdit()));
1080                    x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1081                            top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
1082                            Math.min(activeEnd, mlimit), mUnderlines);
1083
1084                    activeStart = j;
1085                    activePaint.set(wp);
1086                    mUnderlines.clear();
1087                } else {
1088                    // The present TextPaint is substantially equal to the last TextPaint except
1089                    // perhaps for underlines. We just need to expand the active piece of text to
1090                    // include the present chunk, which we always do anyway. We don't need to save
1091                    // wp to activePaint, since they are already equal.
1092                }
1093
1094                activeEnd = jnext;
1095                if (underlineInfo.hasUnderline()) {
1096                    final UnderlineInfo copy = underlineInfo.copyInfo();
1097                    copy.start = j;
1098                    copy.end = jnext;
1099                    mUnderlines.add(copy);
1100                }
1101            }
1102            // Handle the final piece of text.
1103            activePaint.setHyphenEdit(adjustHyphenEdit(
1104                    activeStart, activeEnd, mPaint.getHyphenEdit()));
1105            x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1106                    top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
1107                    Math.min(activeEnd, mlimit), mUnderlines);
1108        }
1109
1110        return x - originalX;
1111    }
1112
1113    /**
1114     * Render a text run with the set-up paint.
1115     *
1116     * @param c the canvas
1117     * @param wp the paint used to render the text
1118     * @param start the start of the run
1119     * @param end the end of the run
1120     * @param contextStart the start of context for the run
1121     * @param contextEnd the end of the context for the run
1122     * @param runIsRtl true if the run is right-to-left
1123     * @param x the x position of the left edge of the run
1124     * @param y the baseline of the run
1125     */
1126    private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
1127            int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
1128
1129        if (mCharsValid) {
1130            int count = end - start;
1131            int contextCount = contextEnd - contextStart;
1132            c.drawTextRun(mChars, start, count, contextStart, contextCount,
1133                    x, y, runIsRtl, wp);
1134        } else {
1135            int delta = mStart;
1136            c.drawTextRun(mText, delta + start, delta + end,
1137                    delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
1138        }
1139    }
1140
1141    /**
1142     * Returns the next tab position.
1143     *
1144     * @param h the (unsigned) offset from the leading margin
1145     * @return the (unsigned) tab position after this offset
1146     */
1147    float nextTab(float h) {
1148        if (mTabs != null) {
1149            return mTabs.nextTab(h);
1150        }
1151        return TabStops.nextDefaultStop(h, TAB_INCREMENT);
1152    }
1153
1154    private boolean isStretchableWhitespace(int ch) {
1155        // TODO: Support other stretchable whitespace. (Bug: 34013491)
1156        return ch == 0x0020 || ch == 0x00A0;
1157    }
1158
1159    private int nextStretchableSpace(int start, int end) {
1160        for (int i = start; i < end; i++) {
1161            final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart);
1162            if (isStretchableWhitespace(c)) return i;
1163        }
1164        return end;
1165    }
1166
1167    /* Return the number of spaces in the text line, for the purpose of justification */
1168    private int countStretchableSpaces(int start, int end) {
1169        int count = 0;
1170        for (int i = start; i < end; i = nextStretchableSpace(i + 1, end)) {
1171            count++;
1172        }
1173        return count;
1174    }
1175
1176    // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
1177    public static boolean isLineEndSpace(char ch) {
1178        return ch == ' ' || ch == '\t' || ch == 0x1680
1179                || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007)
1180                || ch == 0x205F || ch == 0x3000;
1181    }
1182
1183    private static final int TAB_INCREMENT = 20;
1184}
1185