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