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