TextLine.java revision 138b106402d86653eaf296a02708737a6e360b58
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.annotation.NonNull;
20import android.annotation.Nullable;
21import android.graphics.Canvas;
22import android.graphics.Paint;
23import android.graphics.Paint.FontMetricsInt;
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.annotations.VisibleForTesting;
32import com.android.internal.util.ArrayUtils;
33
34import java.util.ArrayList;
35
36/**
37 * Represents a line of styled text, for measuring in visual order and
38 * for rendering.
39 *
40 * <p>Get a new instance using obtain(), and when finished with it, return it
41 * to the pool using recycle().
42 *
43 * <p>Call set to prepare the instance for use, then either draw, measure,
44 * metrics, or caretToLeftRightOf.
45 *
46 * @hide
47 */
48@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
49public class TextLine {
50    private static final boolean DEBUG = false;
51
52    private TextPaint mPaint;
53    private CharSequence mText;
54    private int mStart;
55    private int mLen;
56    private int mDir;
57    private Directions mDirections;
58    private boolean mHasTabs;
59    private TabStops mTabs;
60    private char[] mChars;
61    private boolean mCharsValid;
62    private Spanned mSpanned;
63    private PrecomputedText mComputed;
64
65    // Additional width of whitespace for justification. This value is per whitespace, thus
66    // the line width will increase by mAddedWidth x (number of stretchable whitespaces).
67    private float mAddedWidth;
68
69    private final TextPaint mWorkPaint = new TextPaint();
70    private final TextPaint mActivePaint = new TextPaint();
71    private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =
72            new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);
73    private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =
74            new SpanSet<CharacterStyle>(CharacterStyle.class);
75    private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =
76            new SpanSet<ReplacementSpan>(ReplacementSpan.class);
77
78    private final DecorationInfo mDecorationInfo = new DecorationInfo();
79    private final ArrayList<DecorationInfo> mDecorations = new ArrayList<>();
80
81    private static final TextLine[] sCached = new TextLine[3];
82
83    /**
84     * Returns a new TextLine from the shared pool.
85     *
86     * @return an uninitialized TextLine
87     */
88    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
89    public static TextLine obtain() {
90        TextLine tl;
91        synchronized (sCached) {
92            for (int i = sCached.length; --i >= 0;) {
93                if (sCached[i] != null) {
94                    tl = sCached[i];
95                    sCached[i] = null;
96                    return tl;
97                }
98            }
99        }
100        tl = new TextLine();
101        if (DEBUG) {
102            Log.v("TLINE", "new: " + tl);
103        }
104        return tl;
105    }
106
107    /**
108     * Puts a TextLine back into the shared pool. Do not use this TextLine once
109     * it has been returned.
110     * @param tl the textLine
111     * @return null, as a convenience from clearing references to the provided
112     * TextLine
113     */
114    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
115    public static TextLine recycle(TextLine tl) {
116        tl.mText = null;
117        tl.mPaint = null;
118        tl.mDirections = null;
119        tl.mSpanned = null;
120        tl.mTabs = null;
121        tl.mChars = null;
122        tl.mComputed = null;
123
124        tl.mMetricAffectingSpanSpanSet.recycle();
125        tl.mCharacterStyleSpanSet.recycle();
126        tl.mReplacementSpanSpanSet.recycle();
127
128        synchronized(sCached) {
129            for (int i = 0; i < sCached.length; ++i) {
130                if (sCached[i] == null) {
131                    sCached[i] = tl;
132                    break;
133                }
134            }
135        }
136        return null;
137    }
138
139    /**
140     * Initializes a TextLine and prepares it for use.
141     *
142     * @param paint the base paint for the line
143     * @param text the text, can be Styled
144     * @param start the start of the line relative to the text
145     * @param limit the limit of the line relative to the text
146     * @param dir the paragraph direction of this line
147     * @param directions the directions information of this line
148     * @param hasTabs true if the line might contain tabs
149     * @param tabStops the tabStops. Can be null.
150     */
151    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
152    public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
153            Directions directions, boolean hasTabs, TabStops tabStops) {
154        mPaint = paint;
155        mText = text;
156        mStart = start;
157        mLen = limit - start;
158        mDir = dir;
159        mDirections = directions;
160        if (mDirections == null) {
161            throw new IllegalArgumentException("Directions cannot be null");
162        }
163        mHasTabs = hasTabs;
164        mSpanned = null;
165
166        boolean hasReplacement = false;
167        if (text instanceof Spanned) {
168            mSpanned = (Spanned) text;
169            mReplacementSpanSpanSet.init(mSpanned, start, limit);
170            hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
171        }
172
173        mComputed = null;
174        if (text instanceof PrecomputedText) {
175            // Here, no need to check line break strategy or hyphenation frequency since there is no
176            // line break concept here.
177            mComputed = (PrecomputedText) text;
178            if (!mComputed.getParams().getTextPaint().equalsForTextMeasurement(paint)) {
179                mComputed = null;
180            }
181        }
182
183        mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT;
184
185        if (mCharsValid) {
186            if (mChars == null || mChars.length < mLen) {
187                mChars = ArrayUtils.newUnpaddedCharArray(mLen);
188            }
189            TextUtils.getChars(text, start, limit, mChars, 0);
190            if (hasReplacement) {
191                // Handle these all at once so we don't have to do it as we go.
192                // Replace the first character of each replacement run with the
193                // object-replacement character and the remainder with zero width
194                // non-break space aka BOM.  Cursor movement code skips these
195                // zero-width characters.
196                char[] chars = mChars;
197                for (int i = start, inext; i < limit; i = inext) {
198                    inext = mReplacementSpanSpanSet.getNextTransition(i, limit);
199                    if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)) {
200                        // transition into a span
201                        chars[i - start] = '\ufffc';
202                        for (int j = i - start + 1, e = inext - start; j < e; ++j) {
203                            chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
204                        }
205                    }
206                }
207            }
208        }
209        mTabs = tabStops;
210        mAddedWidth = 0;
211    }
212
213    /**
214     * Justify the line to the given width.
215     */
216    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
217    public void justify(float justifyWidth) {
218        int end = mLen;
219        while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) {
220            end--;
221        }
222        final int spaces = countStretchableSpaces(0, end);
223        if (spaces == 0) {
224            // There are no stretchable spaces, so we can't help the justification by adding any
225            // width.
226            return;
227        }
228        final float width = Math.abs(measure(end, false, null));
229        mAddedWidth = (justifyWidth - width) / spaces;
230    }
231
232    /**
233     * Renders the TextLine.
234     *
235     * @param c the canvas to render on
236     * @param x the leading margin position
237     * @param top the top of the line
238     * @param y the baseline
239     * @param bottom the bottom of the line
240     */
241    void draw(Canvas c, float x, int top, int y, int bottom) {
242        if (!mHasTabs) {
243            if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
244                drawRun(c, 0, mLen, false, x, top, y, bottom, false);
245                return;
246            }
247            if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
248                drawRun(c, 0, mLen, true, x, top, y, bottom, false);
249                return;
250            }
251        }
252
253        float h = 0;
254        int[] runs = mDirections.mDirections;
255
256        int lastRunIndex = runs.length - 2;
257        for (int i = 0; i < runs.length; i += 2) {
258            int runStart = runs[i];
259            int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
260            if (runLimit > mLen) {
261                runLimit = mLen;
262            }
263            boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
264
265            int segstart = runStart;
266            for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
267                int codept = 0;
268                if (mHasTabs && j < runLimit) {
269                    codept = mChars[j];
270                    if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
271                        codept = Character.codePointAt(mChars, j);
272                        if (codept > 0xFFFF) {
273                            ++j;
274                            continue;
275                        }
276                    }
277                }
278
279                if (j == runLimit || codept == '\t') {
280                    h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom,
281                            i != lastRunIndex || j != mLen);
282
283                    if (codept == '\t') {
284                        h = mDir * nextTab(h * mDir);
285                    }
286                    segstart = j + 1;
287                }
288            }
289        }
290    }
291
292    /**
293     * Returns metrics information for the entire line.
294     *
295     * @param fmi receives font metrics information, can be null
296     * @return the signed width of the line
297     */
298    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
299    public float metrics(FontMetricsInt fmi) {
300        return measure(mLen, false, fmi);
301    }
302
303    /**
304     * Returns information about a position on the line.
305     *
306     * @param offset the line-relative character offset, between 0 and the
307     * line length, inclusive
308     * @param trailing true to measure the trailing edge of the character
309     * before offset, false to measure the leading edge of the character
310     * at offset.
311     * @param fmi receives metrics information about the requested
312     * character, can be null.
313     * @return the signed offset from the leading margin to the requested
314     * character edge.
315     */
316    float measure(int offset, boolean trailing, FontMetricsInt fmi) {
317        int target = trailing ? offset - 1 : offset;
318        if (target < 0) {
319            return 0;
320        }
321
322        float h = 0;
323
324        if (!mHasTabs) {
325            if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
326                return measureRun(0, offset, mLen, false, fmi);
327            }
328            if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
329                return measureRun(0, offset, mLen, true, fmi);
330            }
331        }
332
333        char[] chars = mChars;
334        int[] runs = mDirections.mDirections;
335        for (int i = 0; i < runs.length; i += 2) {
336            int runStart = runs[i];
337            int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
338            if (runLimit > mLen) {
339                runLimit = mLen;
340            }
341            boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
342
343            int segstart = runStart;
344            for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
345                int codept = 0;
346                if (mHasTabs && j < runLimit) {
347                    codept = chars[j];
348                    if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
349                        codept = Character.codePointAt(chars, j);
350                        if (codept > 0xFFFF) {
351                            ++j;
352                            continue;
353                        }
354                    }
355                }
356
357                if (j == runLimit || codept == '\t') {
358                    boolean inSegment = target >= segstart && target < j;
359
360                    boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
361                    if (inSegment && advance) {
362                        return h + measureRun(segstart, offset, j, runIsRtl, fmi);
363                    }
364
365                    float w = measureRun(segstart, j, j, runIsRtl, fmi);
366                    h += advance ? w : -w;
367
368                    if (inSegment) {
369                        return h + measureRun(segstart, offset, j, runIsRtl, null);
370                    }
371
372                    if (codept == '\t') {
373                        if (offset == j) {
374                            return h;
375                        }
376                        h = mDir * nextTab(h * mDir);
377                        if (target == j) {
378                            return h;
379                        }
380                    }
381
382                    segstart = j + 1;
383                }
384            }
385        }
386
387        return h;
388    }
389
390    /**
391     * @see #measure(int, boolean, FontMetricsInt)
392     * @return The measure results for all possible offsets
393     */
394    float[] measureAllOffsets(boolean[] trailing, FontMetricsInt fmi) {
395        float[] measurement = new float[mLen + 1];
396
397        int[] target = new int[mLen + 1];
398        for (int offset = 0; offset < target.length; ++offset) {
399            target[offset] = trailing[offset] ? offset - 1 : offset;
400        }
401        if (target[0] < 0) {
402            measurement[0] = 0;
403        }
404
405        float h = 0;
406
407        if (!mHasTabs) {
408            if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
409                for (int offset = 0; offset <= mLen; ++offset) {
410                    measurement[offset] = measureRun(0, offset, mLen, false, fmi);
411                }
412                return measurement;
413            }
414            if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
415                for (int offset = 0; offset <= mLen; ++offset) {
416                    measurement[offset] = measureRun(0, offset, mLen, true, fmi);
417                }
418                return measurement;
419            }
420        }
421
422        char[] chars = mChars;
423        int[] runs = mDirections.mDirections;
424        for (int i = 0; i < runs.length; i += 2) {
425            int runStart = runs[i];
426            int runLimit = runStart + (runs[i + 1] & Layout.RUN_LENGTH_MASK);
427            if (runLimit > mLen) {
428                runLimit = mLen;
429            }
430            boolean runIsRtl = (runs[i + 1] & Layout.RUN_RTL_FLAG) != 0;
431
432            int segstart = runStart;
433            for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; ++j) {
434                int codept = 0;
435                if (mHasTabs && j < runLimit) {
436                    codept = chars[j];
437                    if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
438                        codept = Character.codePointAt(chars, j);
439                        if (codept > 0xFFFF) {
440                            ++j;
441                            continue;
442                        }
443                    }
444                }
445
446                if (j == runLimit || codept == '\t') {
447                    float oldh = h;
448                    boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
449                    float w = measureRun(segstart, j, j, runIsRtl, fmi);
450                    h += advance ? w : -w;
451
452                    float baseh = advance ? oldh : h;
453                    FontMetricsInt crtfmi = advance ? fmi : null;
454                    for (int offset = segstart; offset <= j && offset <= mLen; ++offset) {
455                        if (target[offset] >= segstart && target[offset] < j) {
456                            measurement[offset] =
457                                    baseh + measureRun(segstart, offset, j, runIsRtl, crtfmi);
458                        }
459                    }
460
461                    if (codept == '\t') {
462                        if (target[j] == j) {
463                            measurement[j] = h;
464                        }
465                        h = mDir * nextTab(h * mDir);
466                        if (target[j + 1] == j) {
467                            measurement[j + 1] =  h;
468                        }
469                    }
470
471                    segstart = j + 1;
472                }
473            }
474        }
475        if (target[mLen] == mLen) {
476            measurement[mLen] = h;
477        }
478
479        return measurement;
480    }
481
482    /**
483     * Draws a unidirectional (but possibly multi-styled) run of text.
484     *
485     *
486     * @param c the canvas to draw on
487     * @param start the line-relative start
488     * @param limit the line-relative limit
489     * @param runIsRtl true if the run is right-to-left
490     * @param x the position of the run that is closest to the leading margin
491     * @param top the top of the line
492     * @param y the baseline
493     * @param bottom the bottom of the line
494     * @param needWidth true if the width value is required.
495     * @return the signed width of the run, based on the paragraph direction.
496     * Only valid if needWidth is true.
497     */
498    private float drawRun(Canvas c, int start,
499            int limit, boolean runIsRtl, float x, int top, int y, int bottom,
500            boolean needWidth) {
501
502        if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
503            float w = -measureRun(start, limit, limit, runIsRtl, null);
504            handleRun(start, limit, limit, runIsRtl, c, x + w, top,
505                    y, bottom, null, false);
506            return w;
507        }
508
509        return handleRun(start, limit, limit, runIsRtl, c, x, top,
510                y, bottom, null, needWidth);
511    }
512
513    /**
514     * Measures a unidirectional (but possibly multi-styled) run of text.
515     *
516     *
517     * @param start the line-relative start of the run
518     * @param offset the offset to measure to, between start and limit inclusive
519     * @param limit the line-relative limit of the run
520     * @param runIsRtl true if the run is right-to-left
521     * @param fmi receives metrics information about the requested
522     * run, can be null.
523     * @return the signed width from the start of the run to the leading edge
524     * of the character at offset, based on the run (not paragraph) direction
525     */
526    private float measureRun(int start, int offset, int limit, boolean runIsRtl,
527            FontMetricsInt fmi) {
528        return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true);
529    }
530
531    /**
532     * Walk the cursor through this line, skipping conjuncts and
533     * zero-width characters.
534     *
535     * <p>This function cannot properly walk the cursor off the ends of the line
536     * since it does not know about any shaping on the previous/following line
537     * that might affect the cursor position. Callers must either avoid these
538     * situations or handle the result specially.
539     *
540     * @param cursor the starting position of the cursor, between 0 and the
541     * length of the line, inclusive
542     * @param toLeft true if the caret is moving to the left.
543     * @return the new offset.  If it is less than 0 or greater than the length
544     * of the line, the previous/following line should be examined to get the
545     * actual offset.
546     */
547    int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
548        // 1) The caret marks the leading edge of a character. The character
549        // logically before it might be on a different level, and the active caret
550        // position is on the character at the lower level. If that character
551        // was the previous character, the caret is on its trailing edge.
552        // 2) Take this character/edge and move it in the indicated direction.
553        // This gives you a new character and a new edge.
554        // 3) This position is between two visually adjacent characters.  One of
555        // these might be at a lower level.  The active position is on the
556        // character at the lower level.
557        // 4) If the active position is on the trailing edge of the character,
558        // the new caret position is the following logical character, else it
559        // is the character.
560
561        int lineStart = 0;
562        int lineEnd = mLen;
563        boolean paraIsRtl = mDir == -1;
564        int[] runs = mDirections.mDirections;
565
566        int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
567        boolean trailing = false;
568
569        if (cursor == lineStart) {
570            runIndex = -2;
571        } else if (cursor == lineEnd) {
572            runIndex = runs.length;
573        } else {
574          // First, get information about the run containing the character with
575          // the active caret.
576          for (runIndex = 0; runIndex < runs.length; runIndex += 2) {
577            runStart = lineStart + runs[runIndex];
578            if (cursor >= runStart) {
579              runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK);
580              if (runLimit > lineEnd) {
581                  runLimit = lineEnd;
582              }
583              if (cursor < runLimit) {
584                runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
585                    Layout.RUN_LEVEL_MASK;
586                if (cursor == runStart) {
587                  // The caret is on a run boundary, see if we should
588                  // use the position on the trailing edge of the previous
589                  // logical character instead.
590                  int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;
591                  int pos = cursor - 1;
592                  for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {
593                    prevRunStart = lineStart + runs[prevRunIndex];
594                    if (pos >= prevRunStart) {
595                      prevRunLimit = prevRunStart +
596                          (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK);
597                      if (prevRunLimit > lineEnd) {
598                          prevRunLimit = lineEnd;
599                      }
600                      if (pos < prevRunLimit) {
601                        prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT)
602                            & Layout.RUN_LEVEL_MASK;
603                        if (prevRunLevel < runLevel) {
604                          // Start from logically previous character.
605                          runIndex = prevRunIndex;
606                          runLevel = prevRunLevel;
607                          runStart = prevRunStart;
608                          runLimit = prevRunLimit;
609                          trailing = true;
610                          break;
611                        }
612                      }
613                    }
614                  }
615                }
616                break;
617              }
618            }
619          }
620
621          // caret might be == lineEnd.  This is generally a space or paragraph
622          // separator and has an associated run, but might be the end of
623          // text, in which case it doesn't.  If that happens, we ran off the
624          // end of the run list, and runIndex == runs.length.  In this case,
625          // we are at a run boundary so we skip the below test.
626          if (runIndex != runs.length) {
627              boolean runIsRtl = (runLevel & 0x1) != 0;
628              boolean advance = toLeft == runIsRtl;
629              if (cursor != (advance ? runLimit : runStart) || advance != trailing) {
630                  // Moving within or into the run, so we can move logically.
631                  newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit,
632                          runIsRtl, cursor, advance);
633                  // If the new position is internal to the run, we're at the strong
634                  // position already so we're finished.
635                  if (newCaret != (advance ? runLimit : runStart)) {
636                      return newCaret;
637                  }
638              }
639          }
640        }
641
642        // If newCaret is -1, we're starting at a run boundary and crossing
643        // into another run. Otherwise we've arrived at a run boundary, and
644        // need to figure out which character to attach to.  Note we might
645        // need to run this twice, if we cross a run boundary and end up at
646        // another run boundary.
647        while (true) {
648          boolean advance = toLeft == paraIsRtl;
649          int otherRunIndex = runIndex + (advance ? 2 : -2);
650          if (otherRunIndex >= 0 && otherRunIndex < runs.length) {
651            int otherRunStart = lineStart + runs[otherRunIndex];
652            int otherRunLimit = otherRunStart +
653            (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK);
654            if (otherRunLimit > lineEnd) {
655                otherRunLimit = lineEnd;
656            }
657            int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
658                Layout.RUN_LEVEL_MASK;
659            boolean otherRunIsRtl = (otherRunLevel & 1) != 0;
660
661            advance = toLeft == otherRunIsRtl;
662            if (newCaret == -1) {
663                newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart,
664                        otherRunLimit, otherRunIsRtl,
665                        advance ? otherRunStart : otherRunLimit, advance);
666                if (newCaret == (advance ? otherRunLimit : otherRunStart)) {
667                    // Crossed and ended up at a new boundary,
668                    // repeat a second and final time.
669                    runIndex = otherRunIndex;
670                    runLevel = otherRunLevel;
671                    continue;
672                }
673                break;
674            }
675
676            // The new caret is at a boundary.
677            if (otherRunLevel < runLevel) {
678              // The strong character is in the other run.
679              newCaret = advance ? otherRunStart : otherRunLimit;
680            }
681            break;
682          }
683
684          if (newCaret == -1) {
685              // We're walking off the end of the line.  The paragraph
686              // level is always equal to or lower than any internal level, so
687              // the boundaries get the strong caret.
688              newCaret = advance ? mLen + 1 : -1;
689              break;
690          }
691
692          // Else we've arrived at the end of the line.  That's a strong position.
693          // We might have arrived here by crossing over a run with no internal
694          // breaks and dropping out of the above loop before advancing one final
695          // time, so reset the caret.
696          // Note, we use '<=' below to handle a situation where the only run
697          // on the line is a counter-directional run.  If we're not advancing,
698          // we can end up at the 'lineEnd' position but the caret we want is at
699          // the lineStart.
700          if (newCaret <= lineEnd) {
701              newCaret = advance ? lineEnd : lineStart;
702          }
703          break;
704        }
705
706        return newCaret;
707    }
708
709    /**
710     * Returns the next valid offset within this directional run, skipping
711     * conjuncts and zero-width characters.  This should not be called to walk
712     * off the end of the line, since the returned values might not be valid
713     * on neighboring lines.  If the returned offset is less than zero or
714     * greater than the line length, the offset should be recomputed on the
715     * preceding or following line, respectively.
716     *
717     * @param runIndex the run index
718     * @param runStart the start of the run
719     * @param runLimit the limit of the run
720     * @param runIsRtl true if the run is right-to-left
721     * @param offset the offset
722     * @param after true if the new offset should logically follow the provided
723     * offset
724     * @return the new offset
725     */
726    private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit,
727            boolean runIsRtl, int offset, boolean after) {
728
729        if (runIndex < 0 || offset == (after ? mLen : 0)) {
730            // Walking off end of line.  Since we don't know
731            // what cursor positions are available on other lines, we can't
732            // return accurate values.  These are a guess.
733            if (after) {
734                return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;
735            }
736            return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;
737        }
738
739        TextPaint wp = mWorkPaint;
740        wp.set(mPaint);
741        wp.setWordSpacing(mAddedWidth);
742
743        int spanStart = runStart;
744        int spanLimit;
745        if (mSpanned == null) {
746            spanLimit = runLimit;
747        } else {
748            int target = after ? offset + 1 : offset;
749            int limit = mStart + runLimit;
750            while (true) {
751                spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,
752                        MetricAffectingSpan.class) - mStart;
753                if (spanLimit >= target) {
754                    break;
755                }
756                spanStart = spanLimit;
757            }
758
759            MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,
760                    mStart + spanLimit, MetricAffectingSpan.class);
761            spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);
762
763            if (spans.length > 0) {
764                ReplacementSpan replacement = null;
765                for (int j = 0; j < spans.length; j++) {
766                    MetricAffectingSpan span = spans[j];
767                    if (span instanceof ReplacementSpan) {
768                        replacement = (ReplacementSpan)span;
769                    } else {
770                        span.updateMeasureState(wp);
771                    }
772                }
773
774                if (replacement != null) {
775                    // If we have a replacement span, we're moving either to
776                    // the start or end of this span.
777                    return after ? spanLimit : spanStart;
778                }
779            }
780        }
781
782        int dir = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
783        int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;
784        if (mCharsValid) {
785            return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,
786                    dir, offset, cursorOpt);
787        } else {
788            return wp.getTextRunCursor(mText, mStart + spanStart,
789                    mStart + spanLimit, dir, mStart + offset, cursorOpt) - mStart;
790        }
791    }
792
793    /**
794     * @param wp
795     */
796    private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
797        final int previousTop     = fmi.top;
798        final int previousAscent  = fmi.ascent;
799        final int previousDescent = fmi.descent;
800        final int previousBottom  = fmi.bottom;
801        final int previousLeading = fmi.leading;
802
803        wp.getFontMetricsInt(fmi);
804
805        updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
806                previousLeading);
807    }
808
809    static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
810            int previousDescent, int previousBottom, int previousLeading) {
811        fmi.top     = Math.min(fmi.top,     previousTop);
812        fmi.ascent  = Math.min(fmi.ascent,  previousAscent);
813        fmi.descent = Math.max(fmi.descent, previousDescent);
814        fmi.bottom  = Math.max(fmi.bottom,  previousBottom);
815        fmi.leading = Math.max(fmi.leading, previousLeading);
816    }
817
818    private static void drawStroke(TextPaint wp, Canvas c, int color, float position,
819            float thickness, float xleft, float xright, float baseline) {
820        final float strokeTop = baseline + wp.baselineShift + position;
821
822        final int previousColor = wp.getColor();
823        final Paint.Style previousStyle = wp.getStyle();
824        final boolean previousAntiAlias = wp.isAntiAlias();
825
826        wp.setStyle(Paint.Style.FILL);
827        wp.setAntiAlias(true);
828
829        wp.setColor(color);
830        c.drawRect(xleft, strokeTop, xright, strokeTop + thickness, wp);
831
832        wp.setStyle(previousStyle);
833        wp.setColor(previousColor);
834        wp.setAntiAlias(previousAntiAlias);
835    }
836
837    private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
838            boolean runIsRtl, int offset) {
839        if (mCharsValid) {
840            return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset);
841        } else {
842            final int delta = mStart;
843            if (mComputed == null) {
844                // TODO: Enable measured getRunAdvance for ReplacementSpan and RTL text.
845                return wp.getRunAdvance(mText, delta + start, delta + end,
846                        delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
847            } else {
848                return mComputed.getWidth(start + delta, end + delta);
849            }
850        }
851    }
852
853    /**
854     * Utility function for measuring and rendering text.  The text must
855     * not include a tab.
856     *
857     * @param wp the working paint
858     * @param start the start of the text
859     * @param end the end of the text
860     * @param runIsRtl true if the run is right-to-left
861     * @param c the canvas, can be null if rendering is not needed
862     * @param x the edge of the run closest to the leading margin
863     * @param top the top of the line
864     * @param y the baseline
865     * @param bottom the bottom of the line
866     * @param fmi receives metrics information, can be null
867     * @param needWidth true if the width of the run is needed
868     * @param offset the offset for the purpose of measuring
869     * @param decorations the list of locations and paremeters for drawing decorations
870     * @return the signed width of the run based on the run direction; only
871     * valid if needWidth is true
872     */
873    private float handleText(TextPaint wp, int start, int end,
874            int contextStart, int contextEnd, boolean runIsRtl,
875            Canvas c, float x, int top, int y, int bottom,
876            FontMetricsInt fmi, boolean needWidth, int offset,
877            @Nullable ArrayList<DecorationInfo> decorations) {
878
879        wp.setWordSpacing(mAddedWidth);
880        // Get metrics first (even for empty strings or "0" width runs)
881        if (fmi != null) {
882            expandMetricsFromPaint(fmi, wp);
883        }
884
885        // No need to do anything if the run width is "0"
886        if (end == start) {
887            return 0f;
888        }
889
890        float totalWidth = 0;
891
892        final int numDecorations = decorations == null ? 0 : decorations.size();
893        if (needWidth || (c != null && (wp.bgColor != 0 || numDecorations != 0 || runIsRtl))) {
894            totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset);
895        }
896
897        if (c != null) {
898            final float leftX, rightX;
899            if (runIsRtl) {
900                leftX = x - totalWidth;
901                rightX = x;
902            } else {
903                leftX = x;
904                rightX = x + totalWidth;
905            }
906
907            if (wp.bgColor != 0) {
908                int previousColor = wp.getColor();
909                Paint.Style previousStyle = wp.getStyle();
910
911                wp.setColor(wp.bgColor);
912                wp.setStyle(Paint.Style.FILL);
913                c.drawRect(leftX, top, rightX, bottom, wp);
914
915                wp.setStyle(previousStyle);
916                wp.setColor(previousColor);
917            }
918
919            if (numDecorations != 0) {
920                for (int i = 0; i < numDecorations; i++) {
921                    final DecorationInfo info = decorations.get(i);
922
923                    final int decorationStart = Math.max(info.start, start);
924                    final int decorationEnd = Math.min(info.end, offset);
925                    float decorationStartAdvance = getRunAdvance(
926                            wp, start, end, contextStart, contextEnd, runIsRtl, decorationStart);
927                    float decorationEndAdvance = getRunAdvance(
928                            wp, start, end, contextStart, contextEnd, runIsRtl, decorationEnd);
929                    final float decorationXLeft, decorationXRight;
930                    if (runIsRtl) {
931                        decorationXLeft = rightX - decorationEndAdvance;
932                        decorationXRight = rightX - decorationStartAdvance;
933                    } else {
934                        decorationXLeft = leftX + decorationStartAdvance;
935                        decorationXRight = leftX + decorationEndAdvance;
936                    }
937
938                    // Theoretically, there could be cases where both Paint's and TextPaint's
939                    // setUnderLineText() are called. For backward compatibility, we need to draw
940                    // both underlines, the one with custom color first.
941                    if (info.underlineColor != 0) {
942                        drawStroke(wp, c, info.underlineColor, wp.getUnderlinePosition(),
943                                info.underlineThickness, decorationXLeft, decorationXRight, y);
944                    }
945                    if (info.isUnderlineText) {
946                        final float thickness =
947                                Math.max(wp.getUnderlineThickness(), 1.0f);
948                        drawStroke(wp, c, wp.getColor(), wp.getUnderlinePosition(), thickness,
949                                decorationXLeft, decorationXRight, y);
950                    }
951
952                    if (info.isStrikeThruText) {
953                        final float thickness =
954                                Math.max(wp.getStrikeThruThickness(), 1.0f);
955                        drawStroke(wp, c, wp.getColor(), wp.getStrikeThruPosition(), thickness,
956                                decorationXLeft, decorationXRight, y);
957                    }
958                }
959            }
960
961            drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
962                    leftX, y + wp.baselineShift);
963        }
964
965        return runIsRtl ? -totalWidth : totalWidth;
966    }
967
968    /**
969     * Utility function for measuring and rendering a replacement.
970     *
971     *
972     * @param replacement the replacement
973     * @param wp the work paint
974     * @param start the start of the run
975     * @param limit the limit of the run
976     * @param runIsRtl true if the run is right-to-left
977     * @param c the canvas, can be null if not rendering
978     * @param x the edge of the replacement closest to the leading margin
979     * @param top the top of the line
980     * @param y the baseline
981     * @param bottom the bottom of the line
982     * @param fmi receives metrics information, can be null
983     * @param needWidth true if the width of the replacement is needed
984     * @return the signed width of the run based on the run direction; only
985     * valid if needWidth is true
986     */
987    private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
988            int start, int limit, boolean runIsRtl, Canvas c,
989            float x, int top, int y, int bottom, FontMetricsInt fmi,
990            boolean needWidth) {
991
992        float ret = 0;
993
994        int textStart = mStart + start;
995        int textLimit = mStart + limit;
996
997        if (needWidth || (c != null && runIsRtl)) {
998            int previousTop = 0;
999            int previousAscent = 0;
1000            int previousDescent = 0;
1001            int previousBottom = 0;
1002            int previousLeading = 0;
1003
1004            boolean needUpdateMetrics = (fmi != null);
1005
1006            if (needUpdateMetrics) {
1007                previousTop     = fmi.top;
1008                previousAscent  = fmi.ascent;
1009                previousDescent = fmi.descent;
1010                previousBottom  = fmi.bottom;
1011                previousLeading = fmi.leading;
1012            }
1013
1014            ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
1015
1016            if (needUpdateMetrics) {
1017                updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
1018                        previousLeading);
1019            }
1020        }
1021
1022        if (c != null) {
1023            if (runIsRtl) {
1024                x -= ret;
1025            }
1026            replacement.draw(c, mText, textStart, textLimit,
1027                    x, top, y, bottom, wp);
1028        }
1029
1030        return runIsRtl ? -ret : ret;
1031    }
1032
1033    private int adjustHyphenEdit(int start, int limit, int hyphenEdit) {
1034        int result = hyphenEdit;
1035        // Only draw hyphens on first or last run in line. Disable them otherwise.
1036        if (start > 0) { // not the first run
1037            result &= ~Paint.HYPHENEDIT_MASK_START_OF_LINE;
1038        }
1039        if (limit < mLen) { // not the last run
1040            result &= ~Paint.HYPHENEDIT_MASK_END_OF_LINE;
1041        }
1042        return result;
1043    }
1044
1045    private static final class DecorationInfo {
1046        public boolean isStrikeThruText;
1047        public boolean isUnderlineText;
1048        public int underlineColor;
1049        public float underlineThickness;
1050        public int start = -1;
1051        public int end = -1;
1052
1053        public boolean hasDecoration() {
1054            return isStrikeThruText || isUnderlineText || underlineColor != 0;
1055        }
1056
1057        // Copies the info, but not the start and end range.
1058        public DecorationInfo copyInfo() {
1059            final DecorationInfo copy = new DecorationInfo();
1060            copy.isStrikeThruText = isStrikeThruText;
1061            copy.isUnderlineText = isUnderlineText;
1062            copy.underlineColor = underlineColor;
1063            copy.underlineThickness = underlineThickness;
1064            return copy;
1065        }
1066    }
1067
1068    private void extractDecorationInfo(@NonNull TextPaint paint, @NonNull DecorationInfo info) {
1069        info.isStrikeThruText = paint.isStrikeThruText();
1070        if (info.isStrikeThruText) {
1071            paint.setStrikeThruText(false);
1072        }
1073        info.isUnderlineText = paint.isUnderlineText();
1074        if (info.isUnderlineText) {
1075            paint.setUnderlineText(false);
1076        }
1077        info.underlineColor = paint.underlineColor;
1078        info.underlineThickness = paint.underlineThickness;
1079        paint.setUnderlineText(0, 0.0f);
1080    }
1081
1082    /**
1083     * Utility function for handling a unidirectional run.  The run must not
1084     * contain tabs but can contain styles.
1085     *
1086     *
1087     * @param start the line-relative start of the run
1088     * @param measureLimit the offset to measure to, between start and limit inclusive
1089     * @param limit the limit of the run
1090     * @param runIsRtl true if the run is right-to-left
1091     * @param c the canvas, can be null
1092     * @param x the end of the run closest to the leading margin
1093     * @param top the top of the line
1094     * @param y the baseline
1095     * @param bottom the bottom of the line
1096     * @param fmi receives metrics information, can be null
1097     * @param needWidth true if the width is required
1098     * @return the signed width of the run based on the run direction; only
1099     * valid if needWidth is true
1100     */
1101    private float handleRun(int start, int measureLimit,
1102            int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
1103            int bottom, FontMetricsInt fmi, boolean needWidth) {
1104
1105        if (measureLimit < start || measureLimit > limit) {
1106            throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
1107                    + "start (" + start + ") and limit (" + limit + ") bounds");
1108        }
1109
1110        // Case of an empty line, make sure we update fmi according to mPaint
1111        if (start == measureLimit) {
1112            final TextPaint wp = mWorkPaint;
1113            wp.set(mPaint);
1114            if (fmi != null) {
1115                expandMetricsFromPaint(fmi, wp);
1116            }
1117            return 0f;
1118        }
1119
1120        final boolean needsSpanMeasurement;
1121        if (mSpanned == null) {
1122            needsSpanMeasurement = false;
1123        } else {
1124            mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
1125            mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
1126            needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0
1127                    || mCharacterStyleSpanSet.numberOfSpans != 0;
1128        }
1129
1130        if (!needsSpanMeasurement) {
1131            final TextPaint wp = mWorkPaint;
1132            wp.set(mPaint);
1133            wp.setHyphenEdit(adjustHyphenEdit(start, limit, wp.getHyphenEdit()));
1134            return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,
1135                    y, bottom, fmi, needWidth, measureLimit, null);
1136        }
1137
1138        // Shaping needs to take into account context up to metric boundaries,
1139        // but rendering needs to take into account character style boundaries.
1140        // So we iterate through metric runs to get metric bounds,
1141        // then within each metric run iterate through character style runs
1142        // for the run bounds.
1143        final float originalX = x;
1144        for (int i = start, inext; i < measureLimit; i = inext) {
1145            final TextPaint wp = mWorkPaint;
1146            wp.set(mPaint);
1147
1148            inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
1149                    mStart;
1150            int mlimit = Math.min(inext, measureLimit);
1151
1152            ReplacementSpan replacement = null;
1153
1154            for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
1155                // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
1156                // empty by construction. This special case in getSpans() explains the >= & <= tests
1157                if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||
1158                        (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
1159                final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
1160                if (span instanceof ReplacementSpan) {
1161                    replacement = (ReplacementSpan)span;
1162                } else {
1163                    // We might have a replacement that uses the draw
1164                    // state, otherwise measure state would suffice.
1165                    span.updateDrawState(wp);
1166                }
1167            }
1168
1169            if (replacement != null) {
1170                x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
1171                        bottom, fmi, needWidth || mlimit < measureLimit);
1172                continue;
1173            }
1174
1175            final TextPaint activePaint = mActivePaint;
1176            activePaint.set(mPaint);
1177            int activeStart = i;
1178            int activeEnd = mlimit;
1179            final DecorationInfo decorationInfo = mDecorationInfo;
1180            mDecorations.clear();
1181            for (int j = i, jnext; j < mlimit; j = jnext) {
1182                jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
1183                        mStart;
1184
1185                final int offset = Math.min(jnext, mlimit);
1186                wp.set(mPaint);
1187                for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
1188                    // Intentionally using >= and <= as explained above
1189                    if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
1190                            (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
1191
1192                    final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
1193                    span.updateDrawState(wp);
1194                }
1195
1196                extractDecorationInfo(wp, decorationInfo);
1197
1198                if (j == i) {
1199                    // First chunk of text. We can't handle it yet, since we may need to merge it
1200                    // with the next chunk. So we just save the TextPaint for future comparisons
1201                    // and use.
1202                    activePaint.set(wp);
1203                } else if (!wp.hasEqualAttributes(activePaint)) {
1204                    // The style of the present chunk of text is substantially different from the
1205                    // style of the previous chunk. We need to handle the active piece of text
1206                    // and restart with the present chunk.
1207                    activePaint.setHyphenEdit(adjustHyphenEdit(
1208                            activeStart, activeEnd, mPaint.getHyphenEdit()));
1209                    x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1210                            top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
1211                            Math.min(activeEnd, mlimit), mDecorations);
1212
1213                    activeStart = j;
1214                    activePaint.set(wp);
1215                    mDecorations.clear();
1216                } else {
1217                    // The present TextPaint is substantially equal to the last TextPaint except
1218                    // perhaps for decorations. We just need to expand the active piece of text to
1219                    // include the present chunk, which we always do anyway. We don't need to save
1220                    // wp to activePaint, since they are already equal.
1221                }
1222
1223                activeEnd = jnext;
1224                if (decorationInfo.hasDecoration()) {
1225                    final DecorationInfo copy = decorationInfo.copyInfo();
1226                    copy.start = j;
1227                    copy.end = jnext;
1228                    mDecorations.add(copy);
1229                }
1230            }
1231            // Handle the final piece of text.
1232            activePaint.setHyphenEdit(adjustHyphenEdit(
1233                    activeStart, activeEnd, mPaint.getHyphenEdit()));
1234            x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1235                    top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
1236                    Math.min(activeEnd, mlimit), mDecorations);
1237        }
1238
1239        return x - originalX;
1240    }
1241
1242    /**
1243     * Render a text run with the set-up paint.
1244     *
1245     * @param c the canvas
1246     * @param wp the paint used to render the text
1247     * @param start the start of the run
1248     * @param end the end of the run
1249     * @param contextStart the start of context for the run
1250     * @param contextEnd the end of the context for the run
1251     * @param runIsRtl true if the run is right-to-left
1252     * @param x the x position of the left edge of the run
1253     * @param y the baseline of the run
1254     */
1255    private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
1256            int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
1257
1258        if (mCharsValid) {
1259            int count = end - start;
1260            int contextCount = contextEnd - contextStart;
1261            c.drawTextRun(mChars, start, count, contextStart, contextCount,
1262                    x, y, runIsRtl, wp);
1263        } else {
1264            int delta = mStart;
1265            c.drawTextRun(mText, delta + start, delta + end,
1266                    delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
1267        }
1268    }
1269
1270    /**
1271     * Returns the next tab position.
1272     *
1273     * @param h the (unsigned) offset from the leading margin
1274     * @return the (unsigned) tab position after this offset
1275     */
1276    float nextTab(float h) {
1277        if (mTabs != null) {
1278            return mTabs.nextTab(h);
1279        }
1280        return TabStops.nextDefaultStop(h, TAB_INCREMENT);
1281    }
1282
1283    private boolean isStretchableWhitespace(int ch) {
1284        // TODO: Support NBSP and other stretchable whitespace (b/34013491 and b/68204709).
1285        return ch == 0x0020;
1286    }
1287
1288    /* Return the number of spaces in the text line, for the purpose of justification */
1289    private int countStretchableSpaces(int start, int end) {
1290        int count = 0;
1291        for (int i = start; i < end; i++) {
1292            final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart);
1293            if (isStretchableWhitespace(c)) {
1294                count++;
1295            }
1296        }
1297        return count;
1298    }
1299
1300    // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
1301    public static boolean isLineEndSpace(char ch) {
1302        return ch == ' ' || ch == '\t' || ch == 0x1680
1303                || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007)
1304                || ch == 0x205F || ch == 0x3000;
1305    }
1306
1307    private static final int TAB_INCREMENT = 20;
1308}
1309