TextLine.java revision 08836d4ca5e1dab422575f1f0a1ad617a59e6ba0
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, int baseline) {
709        // kStdUnderline_Offset = 1/9, defined in SkTextFormatParams.h
710        final float underlineTop = baseline + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize();
711
712        final int previousColor = wp.getColor();
713        final Paint.Style previousStyle = wp.getStyle();
714        final boolean previousAntiAlias = wp.isAntiAlias();
715
716        wp.setStyle(Paint.Style.FILL);
717        wp.setAntiAlias(true);
718
719        wp.setColor(color);
720        c.drawRect(xleft, underlineTop, xright, underlineTop + thickness, wp);
721
722        wp.setStyle(previousStyle);
723        wp.setColor(previousColor);
724        wp.setAntiAlias(previousAntiAlias);
725    }
726
727    private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
728            boolean runIsRtl, int offset) {
729        if (mCharsValid) {
730            return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset);
731        } else {
732            final int delta = mStart;
733            return wp.getRunAdvance(mText, delta + start, delta + end,
734                    delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
735        }
736    }
737
738    /**
739     * Utility function for measuring and rendering text.  The text must
740     * not include a tab.
741     *
742     * @param wp the working paint
743     * @param start the start of the text
744     * @param end the end of the text
745     * @param runIsRtl true if the run is right-to-left
746     * @param c the canvas, can be null if rendering is not needed
747     * @param x the edge of the run closest to the leading margin
748     * @param top the top of the line
749     * @param y the baseline
750     * @param bottom the bottom of the line
751     * @param fmi receives metrics information, can be null
752     * @param needWidth true if the width of the run is needed
753     * @param offset the offset for the purpose of measuring
754     * @param underlines the list of locations and paremeters for drawing underlines
755     * @return the signed width of the run based on the run direction; only
756     * valid if needWidth is true
757     */
758    private float handleText(TextPaint wp, int start, int end,
759            int contextStart, int contextEnd, boolean runIsRtl,
760            Canvas c, float x, int top, int y, int bottom,
761            FontMetricsInt fmi, boolean needWidth, int offset,
762            @Nullable ArrayList<UnderlineInfo> underlines) {
763
764        wp.setWordSpacing(mAddedWidth);
765        // Get metrics first (even for empty strings or "0" width runs)
766        if (fmi != null) {
767            expandMetricsFromPaint(fmi, wp);
768        }
769
770        // No need to do anything if the run width is "0"
771        if (end == start) {
772            return 0f;
773        }
774
775        float totalWidth = 0;
776
777        final int numUnderlines = underlines == null ? 0 : underlines.size();
778        if (needWidth || (c != null && (wp.bgColor != 0 || numUnderlines != 0 || runIsRtl))) {
779            totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset);
780        }
781
782        if (c != null) {
783            final float leftX, rightX;
784            if (runIsRtl) {
785                leftX = x - totalWidth;
786                rightX = x;
787            } else {
788                leftX = x;
789                rightX = x + totalWidth;
790            }
791
792            if (wp.bgColor != 0) {
793                int previousColor = wp.getColor();
794                Paint.Style previousStyle = wp.getStyle();
795
796                wp.setColor(wp.bgColor);
797                wp.setStyle(Paint.Style.FILL);
798                c.drawRect(leftX, top, rightX, bottom, wp);
799
800                wp.setStyle(previousStyle);
801                wp.setColor(previousColor);
802            }
803
804            if (numUnderlines != 0) {
805                // kStdUnderline_Thickness = 1/18, defined in SkTextFormatParams.h
806                final float defaultThickness = (1.0f / 18.0f) * wp.getTextSize();
807                for (int i = 0; i < numUnderlines; i++) {
808                    final UnderlineInfo info = underlines.get(i);
809
810                    final int underlineStart = Math.max(info.start, start);
811                    final int underlineEnd = Math.min(info.end, offset);
812                    float underlineStartAdvance = getRunAdvance(
813                            wp, start, end, contextStart, contextEnd, runIsRtl, underlineStart);
814                    float underlineEndAdvance = getRunAdvance(
815                            wp, start, end, contextStart, contextEnd, runIsRtl, underlineEnd);
816                    final float underlineXLeft, underlineXRight;
817                    if (runIsRtl) {
818                        underlineXLeft = rightX - underlineEndAdvance;
819                        underlineXRight = rightX - underlineStartAdvance;
820                    } else {
821                        underlineXLeft = leftX + underlineStartAdvance;
822                        underlineXRight = leftX + underlineEndAdvance;
823                    }
824
825                    // Theoretically, there could be cases where both Paint's and TextPaint's
826                    // setUnderLineText() are called. For backward compatibility, we need to draw
827                    // both underlines, the one with custom color first.
828                    if (info.underlineColor != 0) {
829                        drawUnderline(wp, c, wp.underlineColor, wp.underlineThickness,
830                                underlineXLeft, underlineXRight, y);
831                    }
832                    if (info.isUnderlineText) {
833                        drawUnderline(wp, c, wp.getColor(), defaultThickness,
834                                underlineXLeft, underlineXRight, y);
835                    }
836                }
837            }
838
839            drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
840                    leftX, y + wp.baselineShift);
841        }
842
843        return runIsRtl ? -totalWidth : totalWidth;
844    }
845
846    /**
847     * Utility function for measuring and rendering a replacement.
848     *
849     *
850     * @param replacement the replacement
851     * @param wp the work paint
852     * @param start the start of the run
853     * @param limit the limit of the run
854     * @param runIsRtl true if the run is right-to-left
855     * @param c the canvas, can be null if not rendering
856     * @param x the edge of the replacement closest to the leading margin
857     * @param top the top of the line
858     * @param y the baseline
859     * @param bottom the bottom of the line
860     * @param fmi receives metrics information, can be null
861     * @param needWidth true if the width of the replacement is needed
862     * @return the signed width of the run based on the run direction; only
863     * valid if needWidth is true
864     */
865    private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
866            int start, int limit, boolean runIsRtl, Canvas c,
867            float x, int top, int y, int bottom, FontMetricsInt fmi,
868            boolean needWidth) {
869
870        float ret = 0;
871
872        int textStart = mStart + start;
873        int textLimit = mStart + limit;
874
875        if (needWidth || (c != null && runIsRtl)) {
876            int previousTop = 0;
877            int previousAscent = 0;
878            int previousDescent = 0;
879            int previousBottom = 0;
880            int previousLeading = 0;
881
882            boolean needUpdateMetrics = (fmi != null);
883
884            if (needUpdateMetrics) {
885                previousTop     = fmi.top;
886                previousAscent  = fmi.ascent;
887                previousDescent = fmi.descent;
888                previousBottom  = fmi.bottom;
889                previousLeading = fmi.leading;
890            }
891
892            ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
893
894            if (needUpdateMetrics) {
895                updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
896                        previousLeading);
897            }
898        }
899
900        if (c != null) {
901            if (runIsRtl) {
902                x -= ret;
903            }
904            replacement.draw(c, mText, textStart, textLimit,
905                    x, top, y, bottom, wp);
906        }
907
908        return runIsRtl ? -ret : ret;
909    }
910
911    private int adjustHyphenEdit(int start, int limit, int hyphenEdit) {
912        int result = hyphenEdit;
913        // Only draw hyphens on first or last run in line. Disable them otherwise.
914        if (start > 0) { // not the first run
915            result &= ~Paint.HYPHENEDIT_MASK_START_OF_LINE;
916        }
917        if (limit < mLen) { // not the last run
918            result &= ~Paint.HYPHENEDIT_MASK_END_OF_LINE;
919        }
920        return result;
921    }
922
923    private static final class UnderlineInfo {
924        public boolean isUnderlineText;
925        public int underlineColor;
926        public float underlineThickness;
927        public int start = -1;
928        public int end = -1;
929
930        public boolean hasUnderline() {
931            return isUnderlineText || underlineColor != 0;
932        }
933
934        // Copies the info, but not the start and end range.
935        public UnderlineInfo copyInfo() {
936            final UnderlineInfo copy = new UnderlineInfo();
937            copy.isUnderlineText = isUnderlineText;
938            copy.underlineColor = underlineColor;
939            copy.underlineThickness = underlineThickness;
940            return copy;
941        }
942    }
943
944    private void extractUnderlineInfo(@NonNull TextPaint paint, @NonNull UnderlineInfo info) {
945        info.isUnderlineText = paint.isUnderlineText();
946        if (info.isUnderlineText) {
947            paint.setUnderlineText(false);
948        }
949        info.underlineColor = paint.underlineColor;
950        info.underlineThickness = paint.underlineThickness;
951        paint.setUnderlineText(0, 0.0f);
952    }
953
954    /**
955     * Utility function for handling a unidirectional run.  The run must not
956     * contain tabs but can contain styles.
957     *
958     *
959     * @param start the line-relative start of the run
960     * @param measureLimit the offset to measure to, between start and limit inclusive
961     * @param limit the limit of the run
962     * @param runIsRtl true if the run is right-to-left
963     * @param c the canvas, can be null
964     * @param x the end of the run closest to the leading margin
965     * @param top the top of the line
966     * @param y the baseline
967     * @param bottom the bottom of the line
968     * @param fmi receives metrics information, can be null
969     * @param needWidth true if the width is required
970     * @return the signed width of the run based on the run direction; only
971     * valid if needWidth is true
972     */
973    private float handleRun(int start, int measureLimit,
974            int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
975            int bottom, FontMetricsInt fmi, boolean needWidth) {
976
977        if (measureLimit < start || measureLimit > limit) {
978            throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
979                    + "start (" + start + ") and limit (" + limit + ") bounds");
980        }
981
982        // Case of an empty line, make sure we update fmi according to mPaint
983        if (start == measureLimit) {
984            final TextPaint wp = mWorkPaint;
985            wp.set(mPaint);
986            if (fmi != null) {
987                expandMetricsFromPaint(fmi, wp);
988            }
989            return 0f;
990        }
991
992        final boolean needsSpanMeasurement;
993        if (mSpanned == null) {
994            needsSpanMeasurement = false;
995        } else {
996            mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
997            mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
998            needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0
999                    || mCharacterStyleSpanSet.numberOfSpans != 0;
1000        }
1001
1002        if (!needsSpanMeasurement) {
1003            final TextPaint wp = mWorkPaint;
1004            wp.set(mPaint);
1005            wp.setHyphenEdit(adjustHyphenEdit(start, limit, wp.getHyphenEdit()));
1006            return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,
1007                    y, bottom, fmi, needWidth, measureLimit, null);
1008        }
1009
1010        // Shaping needs to take into account context up to metric boundaries,
1011        // but rendering needs to take into account character style boundaries.
1012        // So we iterate through metric runs to get metric bounds,
1013        // then within each metric run iterate through character style runs
1014        // for the run bounds.
1015        final float originalX = x;
1016        for (int i = start, inext; i < measureLimit; i = inext) {
1017            final TextPaint wp = mWorkPaint;
1018            wp.set(mPaint);
1019
1020            inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
1021                    mStart;
1022            int mlimit = Math.min(inext, measureLimit);
1023
1024            ReplacementSpan replacement = null;
1025
1026            for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
1027                // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
1028                // empty by construction. This special case in getSpans() explains the >= & <= tests
1029                if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||
1030                        (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
1031                final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
1032                if (span instanceof ReplacementSpan) {
1033                    replacement = (ReplacementSpan)span;
1034                } else {
1035                    // We might have a replacement that uses the draw
1036                    // state, otherwise measure state would suffice.
1037                    span.updateDrawState(wp);
1038                }
1039            }
1040
1041            if (replacement != null) {
1042                x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
1043                        bottom, fmi, needWidth || mlimit < measureLimit);
1044                continue;
1045            }
1046
1047            final TextPaint activePaint = mActivePaint;
1048            activePaint.set(mPaint);
1049            int activeStart = i;
1050            int activeEnd = mlimit;
1051            final UnderlineInfo underlineInfo = mUnderlineInfo;
1052            mUnderlines.clear();
1053            for (int j = i, jnext; j < mlimit; j = jnext) {
1054                jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
1055                        mStart;
1056
1057                final int offset = Math.min(jnext, mlimit);
1058                wp.set(mPaint);
1059                for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
1060                    // Intentionally using >= and <= as explained above
1061                    if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
1062                            (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
1063
1064                    final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
1065                    span.updateDrawState(wp);
1066                }
1067
1068                extractUnderlineInfo(wp, underlineInfo);
1069
1070                if (j == i) {
1071                    // First chunk of text. We can't handle it yet, since we may need to merge it
1072                    // with the next chunk. So we just save the TextPaint for future comparisons
1073                    // and use.
1074                    activePaint.set(wp);
1075                } else if (!wp.hasEqualAttributes(activePaint)) {
1076                    // The style of the present chunk of text is substantially different from the
1077                    // style of the previous chunk. We need to handle the active piece of text
1078                    // and restart with the present chunk.
1079                    activePaint.setHyphenEdit(adjustHyphenEdit(
1080                            activeStart, activeEnd, mPaint.getHyphenEdit()));
1081                    x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1082                            top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
1083                            Math.min(activeEnd, mlimit), mUnderlines);
1084
1085                    activeStart = j;
1086                    activePaint.set(wp);
1087                    mUnderlines.clear();
1088                } else {
1089                    // The present TextPaint is substantially equal to the last TextPaint except
1090                    // perhaps for underlines. We just need to expand the active piece of text to
1091                    // include the present chunk, which we always do anyway. We don't need to save
1092                    // wp to activePaint, since they are already equal.
1093                }
1094
1095                activeEnd = jnext;
1096                if (underlineInfo.hasUnderline()) {
1097                    final UnderlineInfo copy = underlineInfo.copyInfo();
1098                    copy.start = j;
1099                    copy.end = jnext;
1100                    mUnderlines.add(copy);
1101                }
1102            }
1103            // Handle the final piece of text.
1104            activePaint.setHyphenEdit(adjustHyphenEdit(
1105                    activeStart, activeEnd, mPaint.getHyphenEdit()));
1106            x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1107                    top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
1108                    Math.min(activeEnd, mlimit), mUnderlines);
1109        }
1110
1111        return x - originalX;
1112    }
1113
1114    /**
1115     * Render a text run with the set-up paint.
1116     *
1117     * @param c the canvas
1118     * @param wp the paint used to render the text
1119     * @param start the start of the run
1120     * @param end the end of the run
1121     * @param contextStart the start of context for the run
1122     * @param contextEnd the end of the context for the run
1123     * @param runIsRtl true if the run is right-to-left
1124     * @param x the x position of the left edge of the run
1125     * @param y the baseline of the run
1126     */
1127    private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
1128            int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
1129
1130        if (mCharsValid) {
1131            int count = end - start;
1132            int contextCount = contextEnd - contextStart;
1133            c.drawTextRun(mChars, start, count, contextStart, contextCount,
1134                    x, y, runIsRtl, wp);
1135        } else {
1136            int delta = mStart;
1137            c.drawTextRun(mText, delta + start, delta + end,
1138                    delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
1139        }
1140    }
1141
1142    /**
1143     * Returns the next tab position.
1144     *
1145     * @param h the (unsigned) offset from the leading margin
1146     * @return the (unsigned) tab position after this offset
1147     */
1148    float nextTab(float h) {
1149        if (mTabs != null) {
1150            return mTabs.nextTab(h);
1151        }
1152        return TabStops.nextDefaultStop(h, TAB_INCREMENT);
1153    }
1154
1155    private boolean isStretchableWhitespace(int ch) {
1156        // TODO: Support other stretchable whitespace. (Bug: 34013491)
1157        return ch == 0x0020 || ch == 0x00A0;
1158    }
1159
1160    private int nextStretchableSpace(int start, int end) {
1161        for (int i = start; i < end; i++) {
1162            final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart);
1163            if (isStretchableWhitespace(c)) return i;
1164        }
1165        return end;
1166    }
1167
1168    /* Return the number of spaces in the text line, for the purpose of justification */
1169    private int countStretchableSpaces(int start, int end) {
1170        int count = 0;
1171        for (int i = start; i < end; i = nextStretchableSpace(i + 1, end)) {
1172            count++;
1173        }
1174        return count;
1175    }
1176
1177    // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
1178    public static boolean isLineEndSpace(char ch) {
1179        return ch == ' ' || ch == '\t' || ch == 0x1680
1180                || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007)
1181                || ch == 0x205F || ch == 0x3000;
1182    }
1183
1184    private static final int TAB_INCREMENT = 20;
1185}
1186