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