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