TextLine.java revision 8059e0903e36cbb5cf8b5c5d5d653acc9bbc8402
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 middle = y + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize();
759                // kStdUnderline_Thickness = 1/18, defined in SkTextFormatParams.h
760                float halfHeight = wp.underlineThickness * (1.0f / 18.0f / 2.0f) * wp.getTextSize();
761
762                int previousColor = wp.getColor();
763                Paint.Style previousStyle = wp.getStyle();
764
765                wp.setColor(wp.underlineColor);
766                wp.setStyle(Paint.Style.FILL);
767                c.drawRect(x, middle - halfHeight, x + ret, middle + halfHeight, wp);
768
769                wp.setStyle(previousStyle);
770                wp.setColor(previousColor);
771            }
772
773            drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
774                    x, y + wp.baselineShift);
775        }
776
777        return runIsRtl ? -ret : ret;
778    }
779
780    /**
781     * Utility function for measuring and rendering a replacement.
782     *
783     *
784     * @param replacement the replacement
785     * @param wp the work paint
786     * @param start the start of the run
787     * @param limit the limit of the run
788     * @param runIsRtl true if the run is right-to-left
789     * @param c the canvas, can be null if not rendering
790     * @param x the edge of the replacement closest to the leading margin
791     * @param top the top of the line
792     * @param y the baseline
793     * @param bottom the bottom of the line
794     * @param fmi receives metrics information, can be null
795     * @param needWidth true if the width of the replacement is needed
796     * @return the signed width of the run based on the run direction; only
797     * valid if needWidth is true
798     */
799    private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
800            int start, int limit, boolean runIsRtl, Canvas c,
801            float x, int top, int y, int bottom, FontMetricsInt fmi,
802            boolean needWidth) {
803
804        float ret = 0;
805
806        int textStart = mStart + start;
807        int textLimit = mStart + limit;
808
809        if (needWidth || (c != null && runIsRtl)) {
810            ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
811        }
812
813        if (c != null) {
814            if (runIsRtl) {
815                x -= ret;
816            }
817            replacement.draw(c, mText, textStart, textLimit,
818                    x, top, y, bottom, wp);
819        }
820
821        return runIsRtl ? -ret : ret;
822    }
823
824    /**
825     * Utility function for handling a unidirectional run.  The run must not
826     * contain tabs or emoji but can contain styles.
827     *
828     *
829     * @param start the line-relative start of the run
830     * @param measureLimit the offset to measure to, between start and limit inclusive
831     * @param limit the limit of the run
832     * @param runIsRtl true if the run is right-to-left
833     * @param c the canvas, can be null
834     * @param x the end of the run closest to the leading margin
835     * @param top the top of the line
836     * @param y the baseline
837     * @param bottom the bottom of the line
838     * @param fmi receives metrics information, can be null
839     * @param needWidth true if the width is required
840     * @return the signed width of the run based on the run direction; only
841     * valid if needWidth is true
842     */
843    private float handleRun(int start, int measureLimit,
844            int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
845            int bottom, FontMetricsInt fmi, boolean needWidth) {
846
847        // Case of an empty line, make sure we update fmi according to mPaint
848        if (start == measureLimit) {
849            TextPaint wp = mWorkPaint;
850            wp.set(mPaint);
851            if (fmi != null) {
852                expandMetricsFromPaint(fmi, wp);
853            }
854            return 0f;
855        }
856
857        // Shaping needs to take into account context up to metric boundaries,
858        // but rendering needs to take into account character style boundaries.
859        // So we iterate through metric runs to get metric bounds,
860        // then within each metric run iterate through character style runs
861        // for the run bounds.
862        float ox = x;
863        for (int i = start, inext; i < measureLimit; i = inext) {
864            TextPaint wp = mWorkPaint;
865            wp.set(mPaint);
866
867            int mlimit;
868            if (mSpanned == null) {
869                inext = limit;
870                mlimit = measureLimit;
871            } else {
872                inext = mSpanned.nextSpanTransition(mStart + i, mStart + limit,
873                        MetricAffectingSpan.class) - mStart;
874
875                mlimit = inext < measureLimit ? inext : measureLimit;
876                MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + i,
877                        mStart + mlimit, MetricAffectingSpan.class);
878                spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);
879
880                if (spans.length > 0) {
881                    ReplacementSpan replacement = null;
882                    for (int j = 0; j < spans.length; j++) {
883                        MetricAffectingSpan span = spans[j];
884                        if (span instanceof ReplacementSpan) {
885                            replacement = (ReplacementSpan)span;
886                        } else {
887                            // We might have a replacement that uses the draw
888                            // state, otherwise measure state would suffice.
889                            span.updateDrawState(wp);
890                        }
891                    }
892
893                    if (replacement != null) {
894                        x += handleReplacement(replacement, wp, i,
895                                mlimit, runIsRtl, c, x, top, y, bottom, fmi,
896                                needWidth || mlimit < measureLimit);
897                        continue;
898                    }
899                }
900            }
901
902            if (mSpanned == null || c == null) {
903                x += handleText(wp, i, mlimit, i, inext, runIsRtl, c, x, top,
904                        y, bottom, fmi, needWidth || mlimit < measureLimit);
905            } else {
906                for (int j = i, jnext; j < mlimit; j = jnext) {
907                    jnext = mSpanned.nextSpanTransition(mStart + j,
908                            mStart + mlimit, CharacterStyle.class) - mStart;
909
910                    CharacterStyle[] spans = mSpanned.getSpans(mStart + j,
911                            mStart + jnext, CharacterStyle.class);
912                    spans = TextUtils.removeEmptySpans(spans, mSpanned, CharacterStyle.class);
913
914                    wp.set(mPaint);
915                    for (int k = 0; k < spans.length; k++) {
916                        CharacterStyle span = spans[k];
917                        span.updateDrawState(wp);
918                    }
919
920                    x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,
921                            top, y, bottom, fmi, needWidth || jnext < measureLimit);
922                }
923            }
924        }
925
926        return x - ox;
927    }
928
929    /**
930     * Render a text run with the set-up paint.
931     *
932     * @param c the canvas
933     * @param wp the paint used to render the text
934     * @param start the start of the run
935     * @param end the end of the run
936     * @param contextStart the start of context for the run
937     * @param contextEnd the end of the context for the run
938     * @param runIsRtl true if the run is right-to-left
939     * @param x the x position of the left edge of the run
940     * @param y the baseline of the run
941     */
942    private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
943            int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
944
945        int flags = runIsRtl ? Canvas.DIRECTION_RTL : Canvas.DIRECTION_LTR;
946        if (mCharsValid) {
947            int count = end - start;
948            int contextCount = contextEnd - contextStart;
949            c.drawTextRun(mChars, start, count, contextStart, contextCount,
950                    x, y, flags, wp);
951        } else {
952            int delta = mStart;
953            c.drawTextRun(mText, delta + start, delta + end,
954                    delta + contextStart, delta + contextEnd, x, y, flags, wp);
955        }
956    }
957
958    /**
959     * Returns the ascent of the text at start.  This is used for scaling
960     * emoji.
961     *
962     * @param pos the line-relative position
963     * @return the ascent of the text at start
964     */
965    float ascent(int pos) {
966        if (mSpanned == null) {
967            return mPaint.ascent();
968        }
969
970        pos += mStart;
971        MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1,
972                MetricAffectingSpan.class);
973        if (spans.length == 0) {
974            return mPaint.ascent();
975        }
976
977        TextPaint wp = mWorkPaint;
978        wp.set(mPaint);
979        for (MetricAffectingSpan span : spans) {
980            span.updateMeasureState(wp);
981        }
982        return wp.ascent();
983    }
984
985    /**
986     * Returns the next tab position.
987     *
988     * @param h the (unsigned) offset from the leading margin
989     * @return the (unsigned) tab position after this offset
990     */
991    float nextTab(float h) {
992        if (mTabs != null) {
993            return mTabs.nextTab(h);
994        }
995        return TabStops.nextDefaultStop(h, TAB_INCREMENT);
996    }
997
998    private static final int TAB_INCREMENT = 20;
999}
1000