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