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