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