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