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