TextLine.java revision 945ee9b1661e60e0074d4f16f61fc147c728c6bf
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        fmi.top     = Math.min(fmi.top,     previousTop);
685        fmi.ascent  = Math.min(fmi.ascent,  previousAscent);
686        fmi.descent = Math.max(fmi.descent, previousDescent);
687        fmi.bottom  = Math.max(fmi.bottom,  previousBottom);
688        fmi.leading = Math.max(fmi.leading, previousLeading);
689    }
690
691    /**
692     * Utility function for measuring and rendering text.  The text must
693     * not include a tab or emoji.
694     *
695     * @param wp the working paint
696     * @param start the start of the text
697     * @param end the end of the text
698     * @param runIsRtl true if the run is right-to-left
699     * @param c the canvas, can be null if rendering is not needed
700     * @param x the edge of the run closest to the leading margin
701     * @param top the top of the line
702     * @param y the baseline
703     * @param bottom the bottom of the line
704     * @param fmi receives metrics information, can be null
705     * @param needWidth true if the width of the run is needed
706     * @return the signed width of the run based on the run direction; only
707     * valid if needWidth is true
708     */
709    private float handleText(TextPaint wp, int start, int end,
710            int contextStart, int contextEnd, boolean runIsRtl,
711            Canvas c, float x, int top, int y, int bottom,
712            FontMetricsInt fmi, boolean needWidth) {
713
714        // Get metrics first (even for empty strings or "0" width runs)
715        if (fmi != null) {
716            expandMetricsFromPaint(fmi, wp);
717        }
718
719        int runLen = end - start;
720        // No need to do anything if the run width is "0"
721        if (runLen == 0) {
722            return 0f;
723        }
724
725        float ret = 0;
726
727        int contextLen = contextEnd - contextStart;
728        if (needWidth || (c != null && (wp.bgColor != 0 || wp.underlineColor != 0 || runIsRtl))) {
729            int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
730            if (mCharsValid) {
731                ret = wp.getTextRunAdvances(mChars, start, runLen,
732                        contextStart, contextLen, flags, null, 0);
733            } else {
734                int delta = mStart;
735                ret = wp.getTextRunAdvances(mText, delta + start,
736                        delta + end, delta + contextStart, delta + contextEnd,
737                        flags, null, 0);
738            }
739        }
740
741        if (c != null) {
742            if (runIsRtl) {
743                x -= ret;
744            }
745
746            if (wp.bgColor != 0) {
747                int previousColor = wp.getColor();
748                Paint.Style previousStyle = wp.getStyle();
749
750                wp.setColor(wp.bgColor);
751                wp.setStyle(Paint.Style.FILL);
752                c.drawRect(x, top, x + ret, bottom, wp);
753
754                wp.setStyle(previousStyle);
755                wp.setColor(previousColor);
756            }
757
758            if (wp.underlineColor != 0) {
759                // kStdUnderline_Offset = 1/9, defined in SkTextFormatParams.h
760                float underlineTop = y + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize();
761
762                int previousColor = wp.getColor();
763                Paint.Style previousStyle = wp.getStyle();
764                boolean previousAntiAlias = wp.isAntiAlias();
765
766                wp.setStyle(Paint.Style.FILL);
767                wp.setAntiAlias(true);
768
769                wp.setColor(wp.underlineColor);
770                c.drawRect(x, underlineTop, x + ret, underlineTop + wp.underlineThickness, wp);
771
772                wp.setStyle(previousStyle);
773                wp.setColor(previousColor);
774                wp.setAntiAlias(previousAntiAlias);
775            }
776
777            drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
778                    x, y + wp.baselineShift);
779        }
780
781        return runIsRtl ? -ret : ret;
782    }
783
784    /**
785     * Utility function for measuring and rendering a replacement.
786     *
787     *
788     * @param replacement the replacement
789     * @param wp the work paint
790     * @param start the start of the run
791     * @param limit the limit of the run
792     * @param runIsRtl true if the run is right-to-left
793     * @param c the canvas, can be null if not rendering
794     * @param x the edge of the replacement closest to the leading margin
795     * @param top the top of the line
796     * @param y the baseline
797     * @param bottom the bottom of the line
798     * @param fmi receives metrics information, can be null
799     * @param needWidth true if the width of the replacement is needed
800     * @return the signed width of the run based on the run direction; only
801     * valid if needWidth is true
802     */
803    private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
804            int start, int limit, boolean runIsRtl, Canvas c,
805            float x, int top, int y, int bottom, FontMetricsInt fmi,
806            boolean needWidth) {
807
808        float ret = 0;
809
810        int textStart = mStart + start;
811        int textLimit = mStart + limit;
812
813        if (needWidth || (c != null && runIsRtl)) {
814            ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
815        }
816
817        if (c != null) {
818            if (runIsRtl) {
819                x -= ret;
820            }
821            replacement.draw(c, mText, textStart, textLimit,
822                    x, top, y, bottom, wp);
823        }
824
825        return runIsRtl ? -ret : ret;
826    }
827
828    private static class SpanSet<E> {
829        final int numberOfSpans;
830        final E[] spans;
831        final int[] spanStarts;
832        final int[] spanEnds;
833        final int[] spanFlags;
834
835        @SuppressWarnings("unchecked")
836        SpanSet(Spanned spanned, int start, int limit, Class<? extends E> type) {
837            final E[] allSpans = spanned.getSpans(start, limit, type);
838            final int length = allSpans.length;
839            // These arrays may end up being too large because of empty spans
840            spans = (E[]) Array.newInstance(type, length);
841            spanStarts = new int[length];
842            spanEnds = new int[length];
843            spanFlags = new int[length];
844
845            int count = 0;
846            for (int i = 0; i < length; i++) {
847                final E span = allSpans[i];
848
849                final int spanStart = spanned.getSpanStart(span);
850                final int spanEnd = spanned.getSpanEnd(span);
851                if (spanStart == spanEnd) continue;
852
853                final int spanFlag = spanned.getSpanFlags(span);
854                final int priority = spanFlag & Spanned.SPAN_PRIORITY;
855                if (priority != 0 && count != 0) {
856                    int j;
857
858                    for (j = 0; j < count; j++) {
859                        final int otherPriority = spanFlags[j] & Spanned.SPAN_PRIORITY;
860                        if (priority > otherPriority) break;
861                    }
862
863                    System.arraycopy(spans, j, spans, j + 1, count - j);
864                    System.arraycopy(spanStarts, j, spanStarts, j + 1, count - j);
865                    System.arraycopy(spanEnds, j, spanEnds, j + 1, count - j);
866                    System.arraycopy(spanFlags, j, spanFlags, j + 1, count - j);
867
868                    spans[j] = span;
869                    spanStarts[j] = spanStart;
870                    spanEnds[j] = spanEnd;
871                    spanFlags[j] = spanFlag;
872                } else {
873                    spans[i] = span;
874                    spanStarts[i] = spanStart;
875                    spanEnds[i] = spanEnd;
876                    spanFlags[i] = spanFlag;
877                }
878
879                count++;
880            }
881            numberOfSpans = count;
882        }
883
884        int getNextTransition(int start, int limit) {
885            for (int i = 0; i < numberOfSpans; i++) {
886                final int spanStart = spanStarts[i];
887                final int spanEnd = spanEnds[i];
888                if (spanStart > start && spanStart < limit) limit = spanStart;
889                if (spanEnd > start && spanEnd < limit) limit = spanEnd;
890            }
891            return limit;
892        }
893    }
894
895    /**
896     * Utility function for handling a unidirectional run.  The run must not
897     * contain tabs or emoji but can contain styles.
898     *
899     *
900     * @param start the line-relative start of the run
901     * @param measureLimit the offset to measure to, between start and limit inclusive
902     * @param limit the limit of the run
903     * @param runIsRtl true if the run is right-to-left
904     * @param c the canvas, can be null
905     * @param x the end of the run closest to the leading margin
906     * @param top the top of the line
907     * @param y the baseline
908     * @param bottom the bottom of the line
909     * @param fmi receives metrics information, can be null
910     * @param needWidth true if the width is required
911     * @return the signed width of the run based on the run direction; only
912     * valid if needWidth is true
913     */
914    private float handleRun(int start, int measureLimit,
915            int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
916            int bottom, FontMetricsInt fmi, boolean needWidth) {
917
918        // Case of an empty line, make sure we update fmi according to mPaint
919        if (start == measureLimit) {
920            TextPaint wp = mWorkPaint;
921            wp.set(mPaint);
922            if (fmi != null) {
923                expandMetricsFromPaint(fmi, wp);
924            }
925            return 0f;
926        }
927
928        if (mSpanned == null) {
929            TextPaint wp = mWorkPaint;
930            wp.set(mPaint);
931            final int mlimit = measureLimit;
932            return handleText(wp, start, mlimit, start, limit, runIsRtl, c, x, top,
933                    y, bottom, fmi, needWidth || mlimit < measureLimit);
934        }
935
936        final SpanSet<MetricAffectingSpan> metricAffectingSpans = new SpanSet<MetricAffectingSpan>(
937                mSpanned, mStart + start, mStart + limit, MetricAffectingSpan.class);
938        final SpanSet<CharacterStyle> characterStyleSpans = new SpanSet<CharacterStyle>(
939                    mSpanned, mStart + start, mStart + limit, CharacterStyle.class);
940
941        // Shaping needs to take into account context up to metric boundaries,
942        // but rendering needs to take into account character style boundaries.
943        // So we iterate through metric runs to get metric bounds,
944        // then within each metric run iterate through character style runs
945        // for the run bounds.
946        final float originalX = x;
947        for (int i = start, inext; i < measureLimit; i = inext) {
948            TextPaint wp = mWorkPaint;
949            wp.set(mPaint);
950
951            inext = metricAffectingSpans.getNextTransition(mStart + i, mStart + limit) - mStart;
952            int mlimit = Math.min(inext, measureLimit);
953
954            ReplacementSpan replacement = null;
955
956            for (int j = 0; j < metricAffectingSpans.numberOfSpans; j++) {
957                // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
958                // empty by construction. This special case in getSpans() explains the >= & <= tests
959                if ((metricAffectingSpans.spanStarts[j] >= mStart + mlimit) ||
960                        (metricAffectingSpans.spanEnds[j] <= mStart + i)) continue;
961                MetricAffectingSpan span = metricAffectingSpans.spans[j];
962                if (span instanceof ReplacementSpan) {
963                    replacement = (ReplacementSpan)span;
964                } else {
965                    // We might have a replacement that uses the draw
966                    // state, otherwise measure state would suffice.
967                    span.updateDrawState(wp);
968                }
969            }
970
971            if (replacement != null) {
972                x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
973                        bottom, fmi, needWidth || mlimit < measureLimit);
974                continue;
975            }
976
977            if (c == null) {
978                x += handleText(wp, i, mlimit, i, inext, runIsRtl, c, x, top,
979                        y, bottom, fmi, needWidth || mlimit < measureLimit);
980            } else {
981                for (int j = i, jnext; j < mlimit; j = jnext) {
982                    jnext = characterStyleSpans.getNextTransition(mStart + j, mStart + mlimit) -
983                            mStart;
984
985                    wp.set(mPaint);
986                    for (int k = 0; k < characterStyleSpans.numberOfSpans; k++) {
987                        // Intentionally using >= and <= as explained above
988                        if ((characterStyleSpans.spanStarts[k] >= mStart + jnext) ||
989                                (characterStyleSpans.spanEnds[k] <= mStart + j)) continue;
990
991                        CharacterStyle span = characterStyleSpans.spans[k];
992                        span.updateDrawState(wp);
993                    }
994
995                    x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,
996                            top, y, bottom, fmi, needWidth || jnext < measureLimit);
997                }
998            }
999        }
1000
1001        return x - originalX;
1002    }
1003
1004    /**
1005     * Render a text run with the set-up paint.
1006     *
1007     * @param c the canvas
1008     * @param wp the paint used to render the text
1009     * @param start the start of the run
1010     * @param end the end of the run
1011     * @param contextStart the start of context for the run
1012     * @param contextEnd the end of the context for the run
1013     * @param runIsRtl true if the run is right-to-left
1014     * @param x the x position of the left edge of the run
1015     * @param y the baseline of the run
1016     */
1017    private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
1018            int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
1019
1020        int flags = runIsRtl ? Canvas.DIRECTION_RTL : Canvas.DIRECTION_LTR;
1021        if (mCharsValid) {
1022            int count = end - start;
1023            int contextCount = contextEnd - contextStart;
1024            c.drawTextRun(mChars, start, count, contextStart, contextCount,
1025                    x, y, flags, wp);
1026        } else {
1027            int delta = mStart;
1028            c.drawTextRun(mText, delta + start, delta + end,
1029                    delta + contextStart, delta + contextEnd, x, y, flags, wp);
1030        }
1031    }
1032
1033    /**
1034     * Returns the ascent of the text at start.  This is used for scaling
1035     * emoji.
1036     *
1037     * @param pos the line-relative position
1038     * @return the ascent of the text at start
1039     */
1040    float ascent(int pos) {
1041        if (mSpanned == null) {
1042            return mPaint.ascent();
1043        }
1044
1045        pos += mStart;
1046        MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1, MetricAffectingSpan.class);
1047        if (spans.length == 0) {
1048            return mPaint.ascent();
1049        }
1050
1051        TextPaint wp = mWorkPaint;
1052        wp.set(mPaint);
1053        for (MetricAffectingSpan span : spans) {
1054            span.updateMeasureState(wp);
1055        }
1056        return wp.ascent();
1057    }
1058
1059    /**
1060     * Returns the next tab position.
1061     *
1062     * @param h the (unsigned) offset from the leading margin
1063     * @return the (unsigned) tab position after this offset
1064     */
1065    float nextTab(float h) {
1066        if (mTabs != null) {
1067            return mTabs.nextTab(h);
1068        }
1069        return TabStops.nextDefaultStop(h, TAB_INCREMENT);
1070    }
1071
1072    private static final int TAB_INCREMENT = 20;
1073}
1074