TextLine.java revision 52edaa9cfb612bd20b0f718dc95e576f55d9367e
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
33import java.lang.reflect.Array;
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    private final TextPaint mWorkPaint = new TextPaint();
62
63    private static final TextLine[] sCached = new TextLine[3];
64
65    /**
66     * Returns a new TextLine from the shared pool.
67     *
68     * @return an uninitialized TextLine
69     */
70    static TextLine obtain() {
71        TextLine tl;
72        synchronized (sCached) {
73            for (int i = sCached.length; --i >= 0;) {
74                if (sCached[i] != null) {
75                    tl = sCached[i];
76                    sCached[i] = null;
77                    return tl;
78                }
79            }
80        }
81        tl = new TextLine();
82        if (DEBUG) {
83            Log.v("TLINE", "new: " + tl);
84        }
85        return tl;
86    }
87
88    /**
89     * Puts a TextLine back into the shared pool. Do not use this TextLine once
90     * it has been returned.
91     * @param tl the textLine
92     * @return null, as a convenience from clearing references to the provided
93     * TextLine
94     */
95    static TextLine recycle(TextLine tl) {
96        tl.mText = null;
97        tl.mPaint = null;
98        tl.mDirections = null;
99        synchronized(sCached) {
100            for (int i = 0; i < sCached.length; ++i) {
101                if (sCached[i] == null) {
102                    sCached[i] = tl;
103                    break;
104                }
105            }
106        }
107        return null;
108    }
109
110    /**
111     * Initializes a TextLine and prepares it for use.
112     *
113     * @param paint the base paint for the line
114     * @param text the text, can be Styled
115     * @param start the start of the line relative to the text
116     * @param limit the limit of the line relative to the text
117     * @param dir the paragraph direction of this line
118     * @param directions the directions information of this line
119     * @param hasTabs true if the line might contain tabs or emoji
120     * @param tabStops the tabStops. Can be null.
121     */
122    void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
123            Directions directions, boolean hasTabs, TabStops tabStops) {
124        mPaint = paint;
125        mText = text;
126        mStart = start;
127        mLen = limit - start;
128        mDir = dir;
129        mDirections = directions;
130        if (mDirections == null) {
131            throw new IllegalArgumentException("Directions cannot be null");
132        }
133        mHasTabs = hasTabs;
134        mSpanned = null;
135
136        boolean hasReplacement = false;
137        if (text instanceof Spanned) {
138            mSpanned = (Spanned) text;
139            ReplacementSpan[] spans = mSpanned.getSpans(start, limit, ReplacementSpan.class);
140            spans = TextUtils.removeEmptySpans(spans, mSpanned, ReplacementSpan.class);
141            hasReplacement = spans.length > 0;
142        }
143
144        mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT;
145
146        if (mCharsValid) {
147            if (mChars == null || mChars.length < mLen) {
148                mChars = new char[ArrayUtils.idealCharArraySize(mLen)];
149            }
150            TextUtils.getChars(text, start, limit, mChars, 0);
151            if (hasReplacement) {
152                // Handle these all at once so we don't have to do it as we go.
153                // Replace the first character of each replacement run with the
154                // object-replacement character and the remainder with zero width
155                // non-break space aka BOM.  Cursor movement code skips these
156                // zero-width characters.
157                char[] chars = mChars;
158                for (int i = start, inext; i < limit; i = inext) {
159                    inext = mSpanned.nextSpanTransition(i, limit, ReplacementSpan.class);
160                    ReplacementSpan[] spans = mSpanned.getSpans(i, inext, ReplacementSpan.class);
161                    spans = TextUtils.removeEmptySpans(spans, mSpanned, ReplacementSpan.class);
162                    if (spans.length > 0) {
163                        // transition into a span
164                        chars[i - start] = '\ufffc';
165                        for (int j = i - start + 1, e = inext - start; j < e; ++j) {
166                            chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
167                        }
168                    }
169                }
170            }
171        }
172        mTabs = tabStops;
173    }
174
175    /**
176     * Renders the TextLine.
177     *
178     * @param c the canvas to render on
179     * @param x the leading margin position
180     * @param top the top of the line
181     * @param y the baseline
182     * @param bottom the bottom of the line
183     */
184    void draw(Canvas c, float x, int top, int y, int bottom) {
185        if (!mHasTabs) {
186            if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
187                drawRun(c, 0, mLen, false, x, top, y, bottom, false);
188                return;
189            }
190            if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
191                drawRun(c, 0, mLen, true, x, top, y, bottom, false);
192                return;
193            }
194        }
195
196        float h = 0;
197        int[] runs = mDirections.mDirections;
198        RectF emojiRect = null;
199
200        int lastRunIndex = runs.length - 2;
201        for (int i = 0; i < runs.length; i += 2) {
202            int runStart = runs[i];
203            int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
204            if (runLimit > mLen) {
205                runLimit = mLen;
206            }
207            boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
208
209            int segstart = runStart;
210            for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
211                int codept = 0;
212                Bitmap bm = null;
213
214                if (mHasTabs && j < runLimit) {
215                    codept = mChars[j];
216                    if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) {
217                        codept = Character.codePointAt(mChars, j);
218                        if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) {
219                            bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
220                        } else if (codept > 0xffff) {
221                            ++j;
222                            continue;
223                        }
224                    }
225                }
226
227                if (j == runLimit || codept == '\t' || bm != null) {
228                    h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom,
229                            i != lastRunIndex || j != mLen);
230
231                    if (codept == '\t') {
232                        h = mDir * nextTab(h * mDir);
233                    } else if (bm != null) {
234                        float bmAscent = ascent(j);
235                        float bitmapHeight = bm.getHeight();
236                        float scale = -bmAscent / bitmapHeight;
237                        float width = bm.getWidth() * scale;
238
239                        if (emojiRect == null) {
240                            emojiRect = new RectF();
241                        }
242                        emojiRect.set(x + h, y + bmAscent,
243                                x + h + width, y);
244                        c.drawBitmap(bm, null, emojiRect, mPaint);
245                        h += width;
246                        j++;
247                    }
248                    segstart = j + 1;
249                }
250            }
251        }
252    }
253
254    /**
255     * Returns metrics information for the entire line.
256     *
257     * @param fmi receives font metrics information, can be null
258     * @return the signed width of the line
259     */
260    float metrics(FontMetricsInt fmi) {
261        return measure(mLen, false, fmi);
262    }
263
264    /**
265     * Returns information about a position on the line.
266     *
267     * @param offset the line-relative character offset, between 0 and the
268     * line length, inclusive
269     * @param trailing true to measure the trailing edge of the character
270     * before offset, false to measure the leading edge of the character
271     * at offset.
272     * @param fmi receives metrics information about the requested
273     * character, can be null.
274     * @return the signed offset from the leading margin to the requested
275     * character edge.
276     */
277    float measure(int offset, boolean trailing, FontMetricsInt fmi) {
278        int target = trailing ? offset - 1 : offset;
279        if (target < 0) {
280            return 0;
281        }
282
283        float h = 0;
284
285        if (!mHasTabs) {
286            if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
287                return measureRun(0, offset, mLen, false, fmi);
288            }
289            if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
290                return measureRun(0, offset, mLen, true, fmi);
291            }
292        }
293
294        char[] chars = mChars;
295        int[] runs = mDirections.mDirections;
296        for (int i = 0; i < runs.length; i += 2) {
297            int runStart = runs[i];
298            int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
299            if (runLimit > mLen) {
300                runLimit = mLen;
301            }
302            boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
303
304            int segstart = runStart;
305            for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
306                int codept = 0;
307                Bitmap bm = null;
308
309                if (mHasTabs && j < runLimit) {
310                    codept = chars[j];
311                    if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) {
312                        codept = Character.codePointAt(chars, j);
313                        if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) {
314                            bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
315                        } else if (codept > 0xffff) {
316                            ++j;
317                            continue;
318                        }
319                    }
320                }
321
322                if (j == runLimit || codept == '\t' || bm != null) {
323                    boolean inSegment = target >= segstart && target < j;
324
325                    boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
326                    if (inSegment && advance) {
327                        return h += measureRun(segstart, offset, j, runIsRtl, fmi);
328                    }
329
330                    float w = measureRun(segstart, j, j, runIsRtl, fmi);
331                    h += advance ? w : -w;
332
333                    if (inSegment) {
334                        return h += measureRun(segstart, offset, j, runIsRtl, null);
335                    }
336
337                    if (codept == '\t') {
338                        if (offset == j) {
339                            return h;
340                        }
341                        h = mDir * nextTab(h * mDir);
342                        if (target == j) {
343                            return h;
344                        }
345                    }
346
347                    if (bm != null) {
348                        float bmAscent = ascent(j);
349                        float wid = bm.getWidth() * -bmAscent / bm.getHeight();
350                        h += mDir * wid;
351                        j++;
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
622        int spanStart = runStart;
623        int spanLimit;
624        if (mSpanned == null) {
625            spanLimit = runLimit;
626        } else {
627            int target = after ? offset + 1 : offset;
628            int limit = mStart + runLimit;
629            while (true) {
630                spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,
631                        MetricAffectingSpan.class) - mStart;
632                if (spanLimit >= target) {
633                    break;
634                }
635                spanStart = spanLimit;
636            }
637
638            MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,
639                    mStart + spanLimit, MetricAffectingSpan.class);
640            spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);
641
642            if (spans.length > 0) {
643                ReplacementSpan replacement = null;
644                for (int j = 0; j < spans.length; j++) {
645                    MetricAffectingSpan span = spans[j];
646                    if (span instanceof ReplacementSpan) {
647                        replacement = (ReplacementSpan)span;
648                    } else {
649                        span.updateMeasureState(wp);
650                    }
651                }
652
653                if (replacement != null) {
654                    // If we have a replacement span, we're moving either to
655                    // the start or end of this span.
656                    return after ? spanLimit : spanStart;
657                }
658            }
659        }
660
661        int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
662        int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;
663        if (mCharsValid) {
664            return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,
665                    flags, offset, cursorOpt);
666        } else {
667            return wp.getTextRunCursor(mText, mStart + spanStart,
668                    mStart + spanLimit, flags, mStart + offset, cursorOpt) - mStart;
669        }
670    }
671
672    /**
673     * @param wp
674     */
675    private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
676        final int previousTop     = fmi.top;
677        final int previousAscent  = fmi.ascent;
678        final int previousDescent = fmi.descent;
679        final int previousBottom  = fmi.bottom;
680        final int previousLeading = fmi.leading;
681
682        wp.getFontMetricsInt(fmi);
683
684        updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
685                previousLeading);
686    }
687
688    static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
689            int previousDescent, int previousBottom, int previousLeading) {
690        fmi.top     = Math.min(fmi.top,     previousTop);
691        fmi.ascent  = Math.min(fmi.ascent,  previousAscent);
692        fmi.descent = Math.max(fmi.descent, previousDescent);
693        fmi.bottom  = Math.max(fmi.bottom,  previousBottom);
694        fmi.leading = Math.max(fmi.leading, previousLeading);
695    }
696
697    /**
698     * Utility function for measuring and rendering text.  The text must
699     * not include a tab or emoji.
700     *
701     * @param wp the working paint
702     * @param start the start of the text
703     * @param end the end of the text
704     * @param runIsRtl true if the run is right-to-left
705     * @param c the canvas, can be null if rendering is not needed
706     * @param x the edge of the run closest to the leading margin
707     * @param top the top of the line
708     * @param y the baseline
709     * @param bottom the bottom of the line
710     * @param fmi receives metrics information, can be null
711     * @param needWidth true if the width of the run is needed
712     * @return the signed width of the run based on the run direction; only
713     * valid if needWidth is true
714     */
715    private float handleText(TextPaint wp, int start, int end,
716            int contextStart, int contextEnd, boolean runIsRtl,
717            Canvas c, float x, int top, int y, int bottom,
718            FontMetricsInt fmi, boolean needWidth) {
719
720        // Get metrics first (even for empty strings or "0" width runs)
721        if (fmi != null) {
722            expandMetricsFromPaint(fmi, wp);
723        }
724
725        int runLen = end - start;
726        // No need to do anything if the run width is "0"
727        if (runLen == 0) {
728            return 0f;
729        }
730
731        float ret = 0;
732
733        int contextLen = contextEnd - contextStart;
734        if (needWidth || (c != null && (wp.bgColor != 0 || wp.underlineColor != 0 || runIsRtl))) {
735            int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
736            if (mCharsValid) {
737                ret = wp.getTextRunAdvances(mChars, start, runLen,
738                        contextStart, contextLen, flags, null, 0);
739            } else {
740                int delta = mStart;
741                ret = wp.getTextRunAdvances(mText, delta + start,
742                        delta + end, delta + contextStart, delta + contextEnd,
743                        flags, null, 0);
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 static class SpanSet<E> {
856        final int numberOfSpans;
857        final E[] spans;
858        final int[] spanStarts;
859        final int[] spanEnds;
860        final int[] spanFlags;
861
862        @SuppressWarnings("unchecked")
863        SpanSet(Spanned spanned, int start, int limit, Class<? extends E> type) {
864            final E[] allSpans = spanned.getSpans(start, limit, type);
865            final int length = allSpans.length;
866            // These arrays may end up being too large because of empty spans
867            spans = (E[]) Array.newInstance(type, length);
868            spanStarts = new int[length];
869            spanEnds = new int[length];
870            spanFlags = new int[length];
871
872            int count = 0;
873            for (int i = 0; i < length; i++) {
874                final E span = allSpans[i];
875
876                final int spanStart = spanned.getSpanStart(span);
877                final int spanEnd = spanned.getSpanEnd(span);
878                if (spanStart == spanEnd) continue;
879
880                final int spanFlag = spanned.getSpanFlags(span);
881                final int priority = spanFlag & Spanned.SPAN_PRIORITY;
882                if (priority != 0 && count != 0) {
883                    int j;
884
885                    for (j = 0; j < count; j++) {
886                        final int otherPriority = spanFlags[j] & Spanned.SPAN_PRIORITY;
887                        if (priority > otherPriority) break;
888                    }
889
890                    System.arraycopy(spans, j, spans, j + 1, count - j);
891                    System.arraycopy(spanStarts, j, spanStarts, j + 1, count - j);
892                    System.arraycopy(spanEnds, j, spanEnds, j + 1, count - j);
893                    System.arraycopy(spanFlags, j, spanFlags, j + 1, count - j);
894
895                    spans[j] = span;
896                    spanStarts[j] = spanStart;
897                    spanEnds[j] = spanEnd;
898                    spanFlags[j] = spanFlag;
899                } else {
900                    spans[i] = span;
901                    spanStarts[i] = spanStart;
902                    spanEnds[i] = spanEnd;
903                    spanFlags[i] = spanFlag;
904                }
905
906                count++;
907            }
908            numberOfSpans = count;
909        }
910
911        int getNextTransition(int start, int limit) {
912            for (int i = 0; i < numberOfSpans; i++) {
913                final int spanStart = spanStarts[i];
914                final int spanEnd = spanEnds[i];
915                if (spanStart > start && spanStart < limit) limit = spanStart;
916                if (spanEnd > start && spanEnd < limit) limit = spanEnd;
917            }
918            return limit;
919        }
920    }
921
922    /**
923     * Utility function for handling a unidirectional run.  The run must not
924     * contain tabs or emoji but can contain styles.
925     *
926     *
927     * @param start the line-relative start of the run
928     * @param measureLimit the offset to measure to, between start and limit inclusive
929     * @param limit the limit of the run
930     * @param runIsRtl true if the run is right-to-left
931     * @param c the canvas, can be null
932     * @param x the end of the run closest to the leading margin
933     * @param top the top of the line
934     * @param y the baseline
935     * @param bottom the bottom of the line
936     * @param fmi receives metrics information, can be null
937     * @param needWidth true if the width is required
938     * @return the signed width of the run based on the run direction; only
939     * valid if needWidth is true
940     */
941    private float handleRun(int start, int measureLimit,
942            int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
943            int bottom, FontMetricsInt fmi, boolean needWidth) {
944
945        // Case of an empty line, make sure we update fmi according to mPaint
946        if (start == measureLimit) {
947            TextPaint wp = mWorkPaint;
948            wp.set(mPaint);
949            if (fmi != null) {
950                expandMetricsFromPaint(fmi, wp);
951            }
952            return 0f;
953        }
954
955        if (mSpanned == null) {
956            TextPaint wp = mWorkPaint;
957            wp.set(mPaint);
958            final int mlimit = measureLimit;
959            return handleText(wp, start, mlimit, start, limit, runIsRtl, c, x, top,
960                    y, bottom, fmi, needWidth || mlimit < measureLimit);
961        }
962
963        final SpanSet<MetricAffectingSpan> metricAffectingSpans = new SpanSet<MetricAffectingSpan>(
964                mSpanned, mStart + start, mStart + limit, MetricAffectingSpan.class);
965        final SpanSet<CharacterStyle> characterStyleSpans = new SpanSet<CharacterStyle>(
966                    mSpanned, mStart + start, mStart + limit, CharacterStyle.class);
967
968        // Shaping needs to take into account context up to metric boundaries,
969        // but rendering needs to take into account character style boundaries.
970        // So we iterate through metric runs to get metric bounds,
971        // then within each metric run iterate through character style runs
972        // for the run bounds.
973        final float originalX = x;
974        for (int i = start, inext; i < measureLimit; i = inext) {
975            TextPaint wp = mWorkPaint;
976            wp.set(mPaint);
977
978            inext = metricAffectingSpans.getNextTransition(mStart + i, mStart + limit) - mStart;
979            int mlimit = Math.min(inext, measureLimit);
980
981            ReplacementSpan replacement = null;
982
983            for (int j = 0; j < metricAffectingSpans.numberOfSpans; j++) {
984                // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
985                // empty by construction. This special case in getSpans() explains the >= & <= tests
986                if ((metricAffectingSpans.spanStarts[j] >= mStart + mlimit) ||
987                        (metricAffectingSpans.spanEnds[j] <= mStart + i)) continue;
988                MetricAffectingSpan span = metricAffectingSpans.spans[j];
989                if (span instanceof ReplacementSpan) {
990                    replacement = (ReplacementSpan)span;
991                } else {
992                    // We might have a replacement that uses the draw
993                    // state, otherwise measure state would suffice.
994                    span.updateDrawState(wp);
995                }
996            }
997
998            if (replacement != null) {
999                x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
1000                        bottom, fmi, needWidth || mlimit < measureLimit);
1001                continue;
1002            }
1003
1004            if (c == null) {
1005                x += handleText(wp, i, mlimit, i, inext, runIsRtl, c, x, top,
1006                        y, bottom, fmi, needWidth || mlimit < measureLimit);
1007            } else {
1008                for (int j = i, jnext; j < mlimit; j = jnext) {
1009                    jnext = characterStyleSpans.getNextTransition(mStart + j, mStart + mlimit) -
1010                            mStart;
1011
1012                    wp.set(mPaint);
1013                    for (int k = 0; k < characterStyleSpans.numberOfSpans; k++) {
1014                        // Intentionally using >= and <= as explained above
1015                        if ((characterStyleSpans.spanStarts[k] >= mStart + jnext) ||
1016                                (characterStyleSpans.spanEnds[k] <= mStart + j)) continue;
1017
1018                        CharacterStyle span = characterStyleSpans.spans[k];
1019                        span.updateDrawState(wp);
1020                    }
1021
1022                    x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,
1023                            top, y, bottom, fmi, needWidth || jnext < measureLimit);
1024                }
1025            }
1026        }
1027
1028        return x - originalX;
1029    }
1030
1031    /**
1032     * Render a text run with the set-up paint.
1033     *
1034     * @param c the canvas
1035     * @param wp the paint used to render the text
1036     * @param start the start of the run
1037     * @param end the end of the run
1038     * @param contextStart the start of context for the run
1039     * @param contextEnd the end of the context for the run
1040     * @param runIsRtl true if the run is right-to-left
1041     * @param x the x position of the left edge of the run
1042     * @param y the baseline of the run
1043     */
1044    private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
1045            int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
1046
1047        int flags = runIsRtl ? Canvas.DIRECTION_RTL : Canvas.DIRECTION_LTR;
1048        if (mCharsValid) {
1049            int count = end - start;
1050            int contextCount = contextEnd - contextStart;
1051            c.drawTextRun(mChars, start, count, contextStart, contextCount,
1052                    x, y, flags, wp);
1053        } else {
1054            int delta = mStart;
1055            c.drawTextRun(mText, delta + start, delta + end,
1056                    delta + contextStart, delta + contextEnd, x, y, flags, wp);
1057        }
1058    }
1059
1060    /**
1061     * Returns the ascent of the text at start.  This is used for scaling
1062     * emoji.
1063     *
1064     * @param pos the line-relative position
1065     * @return the ascent of the text at start
1066     */
1067    float ascent(int pos) {
1068        if (mSpanned == null) {
1069            return mPaint.ascent();
1070        }
1071
1072        pos += mStart;
1073        MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1, MetricAffectingSpan.class);
1074        if (spans.length == 0) {
1075            return mPaint.ascent();
1076        }
1077
1078        TextPaint wp = mWorkPaint;
1079        wp.set(mPaint);
1080        for (MetricAffectingSpan span : spans) {
1081            span.updateMeasureState(wp);
1082        }
1083        return wp.ascent();
1084    }
1085
1086    /**
1087     * Returns the next tab position.
1088     *
1089     * @param h the (unsigned) offset from the leading margin
1090     * @return the (unsigned) tab position after this offset
1091     */
1092    float nextTab(float h) {
1093        if (mTabs != null) {
1094            return mTabs.nextTab(h);
1095        }
1096        return TabStops.nextDefaultStop(h, TAB_INCREMENT);
1097    }
1098
1099    private static final int TAB_INCREMENT = 20;
1100}
1101