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