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 
17 package android.text;
18 
19 import android.annotation.IntRange;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.compat.annotation.UnsupportedAppUsage;
23 import android.graphics.Canvas;
24 import android.graphics.Paint;
25 import android.graphics.Paint.FontMetricsInt;
26 import android.graphics.Rect;
27 import android.graphics.RectF;
28 import android.graphics.text.PositionedGlyphs;
29 import android.graphics.text.TextRunShaper;
30 import android.os.Build;
31 import android.text.Layout.Directions;
32 import android.text.Layout.TabStops;
33 import android.text.style.CharacterStyle;
34 import android.text.style.MetricAffectingSpan;
35 import android.text.style.ReplacementSpan;
36 import android.util.Log;
37 
38 import com.android.internal.annotations.VisibleForTesting;
39 import com.android.internal.util.ArrayUtils;
40 
41 import java.util.ArrayList;
42 
43 /**
44  * Represents a line of styled text, for measuring in visual order and
45  * for rendering.
46  *
47  * <p>Get a new instance using obtain(), and when finished with it, return it
48  * to the pool using recycle().
49  *
50  * <p>Call set to prepare the instance for use, then either draw, measure,
51  * metrics, or caretToLeftRightOf.
52  *
53  * @hide
54  */
55 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
56 public class TextLine {
57     private static final boolean DEBUG = false;
58 
59     private static final char TAB_CHAR = '\t';
60 
61     private TextPaint mPaint;
62     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
63     private CharSequence mText;
64     private int mStart;
65     private int mLen;
66     private int mDir;
67     private Directions mDirections;
68     private boolean mHasTabs;
69     private TabStops mTabs;
70     private char[] mChars;
71     private boolean mCharsValid;
72     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
73     private Spanned mSpanned;
74     private PrecomputedText mComputed;
75     private RectF mTmpRectForMeasure;
76     private RectF mTmpRectForPaintAPI;
77     private Rect mTmpRectForPrecompute;
78 
79     // Recycling object for Paint APIs. Do not use outside getRunAdvances method.
80     private Paint.RunInfo mRunInfo;
81 
82     public static final class LineInfo {
83         private int mClusterCount;
84 
getClusterCount()85         public int getClusterCount() {
86             return mClusterCount;
87         }
88 
setClusterCount(int clusterCount)89         public void setClusterCount(int clusterCount) {
90             mClusterCount = clusterCount;
91         }
92     };
93 
94     private boolean mUseFallbackExtent = false;
95 
96     // The start and end of a potentially existing ellipsis on this text line.
97     // We use them to filter out replacement and metric affecting spans on ellipsized away chars.
98     private int mEllipsisStart;
99     private int mEllipsisEnd;
100 
101     // Additional width of whitespace for justification. This value is per whitespace, thus
102     // the line width will increase by mAddedWidthForJustify x (number of stretchable whitespaces).
103     private float mAddedWordSpacingInPx;
104     private float mAddedLetterSpacingInPx;
105     private boolean mIsJustifying;
106 
107     @VisibleForTesting
getAddedWordSpacingInPx()108     public float getAddedWordSpacingInPx() {
109         return mAddedWordSpacingInPx;
110     }
111 
112     @VisibleForTesting
getAddedLetterSpacingInPx()113     public float getAddedLetterSpacingInPx() {
114         return mAddedLetterSpacingInPx;
115     }
116 
117     @VisibleForTesting
isJustifying()118     public boolean isJustifying() {
119         return mIsJustifying;
120     }
121 
122     private final TextPaint mWorkPaint = new TextPaint();
123     private final TextPaint mActivePaint = new TextPaint();
124     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
125     private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =
126             new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);
127     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
128     private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =
129             new SpanSet<CharacterStyle>(CharacterStyle.class);
130     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
131     private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =
132             new SpanSet<ReplacementSpan>(ReplacementSpan.class);
133 
134     private final DecorationInfo mDecorationInfo = new DecorationInfo();
135     private final ArrayList<DecorationInfo> mDecorations = new ArrayList<>();
136 
137     /** Not allowed to access. If it's for memory leak workaround, it was already fixed M. */
138     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
139     private static final TextLine[] sCached = new TextLine[3];
140 
141     /**
142      * Returns a new TextLine from the shared pool.
143      *
144      * @return an uninitialized TextLine
145      */
146     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
147     @UnsupportedAppUsage
obtain()148     public static TextLine obtain() {
149         TextLine tl;
150         synchronized (sCached) {
151             for (int i = sCached.length; --i >= 0;) {
152                 if (sCached[i] != null) {
153                     tl = sCached[i];
154                     sCached[i] = null;
155                     return tl;
156                 }
157             }
158         }
159         tl = new TextLine();
160         if (DEBUG) {
161             Log.v("TLINE", "new: " + tl);
162         }
163         return tl;
164     }
165 
166     /**
167      * Puts a TextLine back into the shared pool. Do not use this TextLine once
168      * it has been returned.
169      * @param tl the textLine
170      * @return null, as a convenience from clearing references to the provided
171      * TextLine
172      */
173     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
recycle(TextLine tl)174     public static TextLine recycle(TextLine tl) {
175         tl.mText = null;
176         tl.mPaint = null;
177         tl.mDirections = null;
178         tl.mSpanned = null;
179         tl.mTabs = null;
180         tl.mChars = null;
181         tl.mComputed = null;
182         tl.mUseFallbackExtent = false;
183 
184         tl.mMetricAffectingSpanSpanSet.recycle();
185         tl.mCharacterStyleSpanSet.recycle();
186         tl.mReplacementSpanSpanSet.recycle();
187 
188         synchronized(sCached) {
189             for (int i = 0; i < sCached.length; ++i) {
190                 if (sCached[i] == null) {
191                     sCached[i] = tl;
192                     break;
193                 }
194             }
195         }
196         return null;
197     }
198 
199     /**
200      * Initializes a TextLine and prepares it for use.
201      *
202      * @param paint the base paint for the line
203      * @param text the text, can be Styled
204      * @param start the start of the line relative to the text
205      * @param limit the limit of the line relative to the text
206      * @param dir the paragraph direction of this line
207      * @param directions the directions information of this line
208      * @param hasTabs true if the line might contain tabs
209      * @param tabStops the tabStops. Can be null
210      * @param ellipsisStart the start of the ellipsis relative to the line
211      * @param ellipsisEnd the end of the ellipsis relative to the line. When there
212      *                    is no ellipsis, this should be equal to ellipsisStart.
213      * @param useFallbackLineSpacing true for enabling fallback line spacing. false for disabling
214      *                              fallback line spacing.
215      */
216     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
set(TextPaint paint, CharSequence text, int start, int limit, int dir, Directions directions, boolean hasTabs, TabStops tabStops, int ellipsisStart, int ellipsisEnd, boolean useFallbackLineSpacing)217     public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
218             Directions directions, boolean hasTabs, TabStops tabStops,
219             int ellipsisStart, int ellipsisEnd, boolean useFallbackLineSpacing) {
220         mPaint = paint;
221         mText = text;
222         mStart = start;
223         mLen = limit - start;
224         mDir = dir;
225         mDirections = directions;
226         mUseFallbackExtent = useFallbackLineSpacing;
227         if (mDirections == null) {
228             throw new IllegalArgumentException("Directions cannot be null");
229         }
230         mHasTabs = hasTabs;
231         mSpanned = null;
232 
233         boolean hasReplacement = false;
234         if (text instanceof Spanned) {
235             mSpanned = (Spanned) text;
236             mReplacementSpanSpanSet.init(mSpanned, start, limit);
237             hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
238         }
239 
240         mComputed = null;
241         if (text instanceof PrecomputedText) {
242             // Here, no need to check line break strategy or hyphenation frequency since there is no
243             // line break concept here.
244             mComputed = (PrecomputedText) text;
245             if (!mComputed.getParams().getTextPaint().equalsForTextMeasurement(paint)) {
246                 mComputed = null;
247             }
248         }
249 
250         mCharsValid = hasReplacement;
251 
252         if (mCharsValid) {
253             if (mChars == null || mChars.length < mLen) {
254                 mChars = ArrayUtils.newUnpaddedCharArray(mLen);
255             }
256             TextUtils.getChars(text, start, limit, mChars, 0);
257             if (hasReplacement) {
258                 // Handle these all at once so we don't have to do it as we go.
259                 // Replace the first character of each replacement run with the
260                 // object-replacement character and the remainder with zero width
261                 // non-break space aka BOM.  Cursor movement code skips these
262                 // zero-width characters.
263                 char[] chars = mChars;
264                 for (int i = start, inext; i < limit; i = inext) {
265                     inext = mReplacementSpanSpanSet.getNextTransition(i, limit);
266                     if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)
267                             && (i - start >= ellipsisEnd || inext - start <= ellipsisStart)) {
268                         // transition into a span
269                         chars[i - start] = '\ufffc';
270                         for (int j = i - start + 1, e = inext - start; j < e; ++j) {
271                             chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
272                         }
273                     }
274                 }
275             }
276         }
277         mTabs = tabStops;
278         mAddedWordSpacingInPx = 0;
279         mIsJustifying = false;
280 
281         mEllipsisStart = ellipsisStart != ellipsisEnd ? ellipsisStart : 0;
282         mEllipsisEnd = ellipsisStart != ellipsisEnd ? ellipsisEnd : 0;
283     }
284 
charAt(int i)285     private char charAt(int i) {
286         return mCharsValid ? mChars[i] : mText.charAt(i + mStart);
287     }
288 
289     /**
290      * Justify the line to the given width.
291      */
292     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
justify(@ayout.JustificationMode int justificationMode, float justifyWidth)293     public void justify(@Layout.JustificationMode int justificationMode, float justifyWidth) {
294         int end = mLen;
295         while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) {
296             end--;
297         }
298         if (justificationMode == Layout.JUSTIFICATION_MODE_INTER_WORD) {
299             float width = Math.abs(measure(end, false, null, null, null));
300             final int spaces = countStretchableSpaces(0, end);
301             if (spaces == 0) {
302                 // There are no stretchable spaces, so we can't help the justification by adding any
303                 // width.
304                 return;
305             }
306             mAddedWordSpacingInPx = (justifyWidth - width) / spaces;
307             mAddedLetterSpacingInPx = 0;
308         } else {  // justificationMode == Layout.JUSTIFICATION_MODE_LETTER_SPACING
309             LineInfo lineInfo = new LineInfo();
310             float width = Math.abs(measure(end, false, null, null, lineInfo));
311 
312             int lettersCount = lineInfo.getClusterCount();
313             if (lettersCount < 2) {
314                 return;
315             }
316             mAddedLetterSpacingInPx = (justifyWidth - width) / (lettersCount - 1);
317             if (mAddedLetterSpacingInPx > 0.03) {
318                 // If the letter spacing is more than 0.03em, the ligatures are automatically
319                 // disabled, so re-calculate everything without ligatures.
320                 final String oldFontFeatures = mPaint.getFontFeatureSettings();
321                 mPaint.setFontFeatureSettings(oldFontFeatures + ", \"liga\" off, \"cliga\" off");
322                 width = Math.abs(measure(end, false, null, null, lineInfo));
323                 lettersCount = lineInfo.getClusterCount();
324                 mAddedLetterSpacingInPx = (justifyWidth - width) / (lettersCount - 1);
325                 mPaint.setFontFeatureSettings(oldFontFeatures);
326             }
327             mAddedWordSpacingInPx = 0;
328         }
329         mIsJustifying = true;
330     }
331 
332     /**
333      * Returns the run flag of at the given BiDi run.
334      *
335      * @param bidiRunIndex a BiDi run index.
336      * @return a run flag of the given BiDi run.
337      */
338     @VisibleForTesting
calculateRunFlag(int bidiRunIndex, int bidiRunCount, int lineDirection)339     public static int calculateRunFlag(int bidiRunIndex, int bidiRunCount, int lineDirection) {
340         if (bidiRunCount == 1) {
341             // Easy case. If there is only single run, it is most left and most right run.
342             return Paint.TEXT_RUN_FLAG_LEFT_EDGE | Paint.TEXT_RUN_FLAG_RIGHT_EDGE;
343         }
344         if (bidiRunIndex != 0 && bidiRunIndex != (bidiRunCount - 1)) {
345             // Easy case. If the given run is the middle of the line, it is not the most left or
346             // the most right run.
347             return 0;
348         }
349 
350         int runFlag = 0;
351         // For the historical reasons, the BiDi implementation of Android works differently
352         // from the Java BiDi APIs. The mDirections holds the BiDi runs in visual order, but
353         // it is reversed order if the paragraph direction is RTL. So, the first BiDi run of
354         // mDirections is located the most left of the line if the paragraph direction is LTR.
355         // If the paragraph direction is RTL, the first BiDi run is located the most right of
356         // the line.
357         if (bidiRunIndex == 0) {
358             if (lineDirection == Layout.DIR_LEFT_TO_RIGHT) {
359                 runFlag |= Paint.TEXT_RUN_FLAG_LEFT_EDGE;
360             } else {
361                 runFlag |= Paint.TEXT_RUN_FLAG_RIGHT_EDGE;
362             }
363         }
364         if (bidiRunIndex == (bidiRunCount - 1)) {
365             if (lineDirection == Layout.DIR_LEFT_TO_RIGHT) {
366                 runFlag |= Paint.TEXT_RUN_FLAG_RIGHT_EDGE;
367             } else {
368                 runFlag |= Paint.TEXT_RUN_FLAG_LEFT_EDGE;
369             }
370         }
371         return runFlag;
372     }
373 
374     /**
375      * Resolve the runFlag for the inline span range.
376      *
377      * @param runFlag the runFlag of the current BiDi run.
378      * @param isRtlRun true for RTL run, false for LTR run.
379      * @param runStart the inclusive BiDi run start offset.
380      * @param runEnd the exclusive BiDi run end offset.
381      * @param spanStart the inclusive span start offset.
382      * @param spanEnd the exclusive span end offset.
383      * @return the resolved runFlag.
384      */
385     @VisibleForTesting
resolveRunFlagForSubSequence(int runFlag, boolean isRtlRun, int runStart, int runEnd, int spanStart, int spanEnd)386     public static int resolveRunFlagForSubSequence(int runFlag, boolean isRtlRun, int runStart,
387             int runEnd, int spanStart, int spanEnd) {
388         if (runFlag == 0) {
389             // Easy case. If the run is in the middle of the line, any inline span is also in the
390             // middle of the line.
391             return 0;
392         }
393         int localRunFlag = runFlag;
394         if ((runFlag & Paint.TEXT_RUN_FLAG_LEFT_EDGE) != 0) {
395             if (isRtlRun) {
396                 if (spanEnd != runEnd) {
397                     // In the RTL context, the last run is the most left run.
398                     localRunFlag &= ~Paint.TEXT_RUN_FLAG_LEFT_EDGE;
399                 }
400             } else {  // LTR
401                 if (spanStart != runStart) {
402                     // In the LTR context, the first run is the most left run.
403                     localRunFlag &= ~Paint.TEXT_RUN_FLAG_LEFT_EDGE;
404                 }
405             }
406         }
407         if ((runFlag & Paint.TEXT_RUN_FLAG_RIGHT_EDGE) != 0) {
408             if (isRtlRun) {
409                 if (spanStart != runStart) {
410                     // In the RTL context, the start of the run is the most right run.
411                     localRunFlag &= ~Paint.TEXT_RUN_FLAG_RIGHT_EDGE;
412                 }
413             } else {  // LTR
414                 if (spanEnd != runEnd) {
415                     // In the LTR context, the last run is the most right position.
416                     localRunFlag &= ~Paint.TEXT_RUN_FLAG_RIGHT_EDGE;
417                 }
418             }
419         }
420         return localRunFlag;
421     }
422 
423     /**
424      * Renders the TextLine.
425      *
426      * @param c the canvas to render on
427      * @param x the leading margin position
428      * @param top the top of the line
429      * @param y the baseline
430      * @param bottom the bottom of the line
431      */
draw(Canvas c, float x, int top, int y, int bottom)432     void draw(Canvas c, float x, int top, int y, int bottom) {
433         float h = 0;
434         final int runCount = mDirections.getRunCount();
435         for (int runIndex = 0; runIndex < runCount; runIndex++) {
436             final int runStart = mDirections.getRunStart(runIndex);
437             if (runStart > mLen) break;
438             final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
439             final boolean runIsRtl = mDirections.isRunRtl(runIndex);
440 
441             final int runFlag = calculateRunFlag(runIndex, runCount, mDir);
442 
443             int segStart = runStart;
444             for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
445                 if (j == runLimit || charAt(j) == TAB_CHAR) {
446                     h += drawRun(c, segStart, j, runIsRtl, x + h, top, y, bottom,
447                             runIndex != (runCount - 1) || j != mLen, runFlag);
448 
449                     if (j != runLimit) {  // charAt(j) == TAB_CHAR
450                         h = mDir * nextTab(h * mDir);
451                     }
452                     segStart = j + 1;
453                 }
454             }
455         }
456     }
457 
458     /**
459      * Returns metrics information for the entire line.
460      *
461      * @param fmi receives font metrics information, can be null
462      * @param drawBounds output parameter for drawing bounding box. optional.
463      * @param returnDrawWidth true for returning width of the bounding box, false for returning
464      *                       total advances.
465      * @param lineInfo an optional output parameter for filling line information.
466      * @return the signed width of the line
467      */
468     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
metrics(FontMetricsInt fmi, @Nullable RectF drawBounds, boolean returnDrawWidth, @Nullable LineInfo lineInfo)469     public float metrics(FontMetricsInt fmi, @Nullable RectF drawBounds, boolean returnDrawWidth,
470             @Nullable LineInfo lineInfo) {
471         if (returnDrawWidth) {
472             if (drawBounds == null) {
473                 if (mTmpRectForMeasure == null) {
474                     mTmpRectForMeasure = new RectF();
475                 }
476                 drawBounds = mTmpRectForMeasure;
477             }
478             drawBounds.setEmpty();
479             float w = measure(mLen, false, fmi, drawBounds, lineInfo);
480             float boundsWidth;
481             if (w >= 0) {
482                 boundsWidth = Math.max(drawBounds.right, w) - Math.min(0, drawBounds.left);
483             } else {
484                 boundsWidth = Math.max(drawBounds.right, 0) - Math.min(w, drawBounds.left);
485             }
486             if (Math.abs(w) > boundsWidth) {
487                 return w;
488             } else {
489                 // bounds width is always positive but output of measure is signed width.
490                 // To be able to use bounds width as signed width, use the sign of the width.
491                 return Math.signum(w) * boundsWidth;
492             }
493         } else {
494             return measure(mLen, false, fmi, drawBounds, lineInfo);
495         }
496     }
497 
498     /**
499      * Shape the TextLine.
500      */
shape(TextShaper.GlyphsConsumer consumer)501     void shape(TextShaper.GlyphsConsumer consumer) {
502         float horizontal = 0;
503         float x = 0;
504         final int runCount = mDirections.getRunCount();
505         for (int runIndex = 0; runIndex < runCount; runIndex++) {
506             final int runStart = mDirections.getRunStart(runIndex);
507             if (runStart > mLen) break;
508             final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
509             final boolean runIsRtl = mDirections.isRunRtl(runIndex);
510 
511             final int runFlag = calculateRunFlag(runIndex, runCount, mDir);
512             int segStart = runStart;
513             for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
514                 if (j == runLimit || charAt(j) == TAB_CHAR) {
515                     horizontal += shapeRun(consumer, segStart, j, runIsRtl, x + horizontal,
516                             runIndex != (runCount - 1) || j != mLen, runFlag);
517 
518                     if (j != runLimit) {  // charAt(j) == TAB_CHAR
519                         horizontal = mDir * nextTab(horizontal * mDir);
520                     }
521                     segStart = j + 1;
522                 }
523             }
524         }
525     }
526 
527     /**
528      * Returns the signed graphical offset from the leading margin.
529      *
530      * Following examples are all for measuring offset=3. LX(e.g. L0, L1, ...) denotes a
531      * character which has LTR BiDi property. On the other hand, RX(e.g. R0, R1, ...) denotes a
532      * character which has RTL BiDi property. Assuming all character has 1em width.
533      *
534      * Example 1: All LTR chars within LTR context
535      *   Input Text (logical)  :   L0 L1 L2 L3 L4 L5 L6 L7 L8
536      *   Input Text (visual)   :   L0 L1 L2 L3 L4 L5 L6 L7 L8
537      *   Output(trailing=true) :  |--------| (Returns 3em)
538      *   Output(trailing=false):  |--------| (Returns 3em)
539      *
540      * Example 2: All RTL chars within RTL context.
541      *   Input Text (logical)  :   R0 R1 R2 R3 R4 R5 R6 R7 R8
542      *   Input Text (visual)   :   R8 R7 R6 R5 R4 R3 R2 R1 R0
543      *   Output(trailing=true) :                    |--------| (Returns -3em)
544      *   Output(trailing=false):                    |--------| (Returns -3em)
545      *
546      * Example 3: BiDi chars within LTR context.
547      *   Input Text (logical)  :   L0 L1 L2 R3 R4 R5 L6 L7 L8
548      *   Input Text (visual)   :   L0 L1 L2 R5 R4 R3 L6 L7 L8
549      *   Output(trailing=true) :  |-----------------| (Returns 6em)
550      *   Output(trailing=false):  |--------| (Returns 3em)
551      *
552      * Example 4: BiDi chars within RTL context.
553      *   Input Text (logical)  :   L0 L1 L2 R3 R4 R5 L6 L7 L8
554      *   Input Text (visual)   :   L6 L7 L8 R5 R4 R3 L0 L1 L2
555      *   Output(trailing=true) :           |-----------------| (Returns -6em)
556      *   Output(trailing=false):                    |--------| (Returns -3em)
557      *
558      * @param offset the line-relative character offset, between 0 and the line length, inclusive
559      * @param trailing no effect if the offset is not on the BiDi transition offset. If the offset
560      *                 is on the BiDi transition offset and true is passed, the offset is regarded
561      *                 as the edge of the trailing run's edge. If false, the offset is regarded as
562      *                 the edge of the preceding run's edge. See example above.
563      * @param fmi receives metrics information about the requested character, can be null
564      * @param drawBounds output parameter for drawing bounding box. optional.
565      * @param lineInfo an optional output parameter for filling line information.
566      * @return the signed graphical offset from the leading margin to the requested character edge.
567      *         The positive value means the offset is right from the leading edge. The negative
568      *         value means the offset is left from the leading edge.
569      */
measure(@ntRangefrom = 0) int offset, boolean trailing, @NonNull FontMetricsInt fmi, @Nullable RectF drawBounds, @Nullable LineInfo lineInfo)570     public float measure(@IntRange(from = 0) int offset, boolean trailing,
571             @NonNull FontMetricsInt fmi, @Nullable RectF drawBounds, @Nullable LineInfo lineInfo) {
572         if (offset > mLen) {
573             throw new IndexOutOfBoundsException(
574                     "offset(" + offset + ") should be less than line limit(" + mLen + ")");
575         }
576         if (lineInfo != null) {
577             lineInfo.setClusterCount(0);
578         }
579         final int target = trailing ? offset - 1 : offset;
580         if (target < 0) {
581             return 0;
582         }
583 
584         float h = 0;
585         final int runCount = mDirections.getRunCount();
586         for (int runIndex = 0; runIndex < runCount; runIndex++) {
587             final int runStart = mDirections.getRunStart(runIndex);
588             if (runStart > mLen) break;
589             final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
590             final boolean runIsRtl = mDirections.isRunRtl(runIndex);
591             final int runFlag = calculateRunFlag(runIndex, runCount, mDir);
592 
593             int segStart = runStart;
594             for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
595                 if (j == runLimit || charAt(j) == TAB_CHAR) {
596                     final boolean targetIsInThisSegment = target >= segStart && target < j;
597                     final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
598 
599                     if (targetIsInThisSegment && sameDirection) {
600                         return h + measureRun(segStart, offset, j, runIsRtl, fmi, drawBounds, null,
601                                 0, h, lineInfo, runFlag);
602                     }
603 
604                     final float segmentWidth = measureRun(segStart, j, j, runIsRtl, fmi, drawBounds,
605                             null, 0, h, lineInfo, runFlag);
606                     h += sameDirection ? segmentWidth : -segmentWidth;
607 
608                     if (targetIsInThisSegment) {
609                         return h + measureRun(segStart, offset, j, runIsRtl, null, null,  null, 0,
610                                 h, lineInfo, runFlag);
611                     }
612 
613                     if (j != runLimit) {  // charAt(j) == TAB_CHAR
614                         if (offset == j) {
615                             return h;
616                         }
617                         h = mDir * nextTab(h * mDir);
618                         if (target == j) {
619                             return h;
620                         }
621                     }
622 
623                     segStart = j + 1;
624                 }
625             }
626         }
627 
628         return h;
629     }
630 
631     /**
632      * Return the signed horizontal bounds of the characters in the line.
633      *
634      * The length of the returned array equals to 2 * mLen. The left bound of the i th character
635      * is stored at index 2 * i. And the right bound of the i th character is stored at index
636      * (2 * i + 1).
637      *
638      * Check the following examples. LX(e.g. L0, L1, ...) denotes a character which has LTR BiDi
639      * property. On the other hand, RX(e.g. R0, R1, ...) denotes a character which has RTL BiDi
640      * property. Assuming all character has 1em width.
641      *
642      * Example 1: All LTR chars within LTR context
643      *   Input Text (logical)  :   L0 L1 L2 L3
644      *   Input Text (visual)   :   L0 L1 L2 L3
645      *   Output :  [0em, 1em, 1em, 2em, 2em, 3em, 3em, 4em]
646      *
647      * Example 2: All RTL chars within RTL context.
648      *   Input Text (logical)  :   R0 R1 R2 R3
649      *   Input Text (visual)   :   R3 R2 R1 R0
650      *   Output :  [-1em, 0em, -2em, -1em, -3em, -2em, -4em, -3em]
651 
652      *
653      * Example 3: BiDi chars within LTR context.
654      *   Input Text (logical)  :   L0 L1 R2 R3 L4 L5
655      *   Input Text (visual)   :   L0 L1 R3 R2 L4 L5
656      *   Output :  [0em, 1em, 1em, 2em, 3em, 4em, 2em, 3em, 4em, 5em, 5em, 6em]
657 
658      *
659      * Example 4: BiDi chars within RTL context.
660      *   Input Text (logical)  :   L0 L1 R2 R3 L4 L5
661      *   Input Text (visual)   :   L4 L5 R3 R2 L0 L1
662      *   Output :  [-2em, -1em, -1em, 0em, -3em, -2em, -4em, -3em, -6em, -5em, -5em, -4em]
663      *
664      * @param bounds the array to receive the character bounds data. Its length should be at least
665      *               2 times of the line length.
666      * @param advances the array to receive the character advance data, nullable. If provided, its
667      *                 length should be equal or larger than the line length.
668      *
669      * @throws IllegalArgumentException if the given {@code bounds} is null.
670      * @throws IndexOutOfBoundsException if the given {@code bounds} or {@code advances} doesn't
671      * have enough space to hold the result.
672      */
673     public void measureAllBounds(@NonNull float[] bounds, @Nullable float[] advances) {
674         if (bounds == null) {
675             throw new IllegalArgumentException("bounds can't be null");
676         }
677         if (bounds.length < 2 * mLen) {
678             throw new IndexOutOfBoundsException("bounds doesn't have enough space to receive the "
679                     + "result, needed: " + (2 * mLen) + " had: " + bounds.length);
680         }
681         if (advances == null) {
682             advances = new float[mLen];
683         }
684         if (advances.length < mLen) {
685             throw new IndexOutOfBoundsException("advance doesn't have enough space to receive the "
686                     + "result, needed: " + mLen + " had: " + advances.length);
687         }
688         float h = 0;
689         final int runCount = mDirections.getRunCount();
690         for (int runIndex = 0; runIndex < runCount; runIndex++) {
691             final int runStart = mDirections.getRunStart(runIndex);
692             if (runStart > mLen) break;
693             final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
694             final boolean runIsRtl = mDirections.isRunRtl(runIndex);
695             final int runFlag = calculateRunFlag(runIndex, runCount, mDir);
696 
697             int segStart = runStart;
698             for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
699                 if (j == runLimit || charAt(j) == TAB_CHAR) {
700                     final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
701                     final float segmentWidth =
702                             measureRun(segStart, j, j, runIsRtl, null, null, advances, segStart, 0,
703                                     null, runFlag);
704 
705                     final float oldh = h;
706                     h += sameDirection ? segmentWidth : -segmentWidth;
707                     float currh = sameDirection ? oldh : h;
708                     for (int offset = segStart; offset < j && offset < mLen; ++offset) {
709                         if (runIsRtl) {
710                             bounds[2 * offset + 1] = currh;
711                             currh -= advances[offset];
712                             bounds[2 * offset] = currh;
713                         } else {
714                             bounds[2 * offset] = currh;
715                             currh += advances[offset];
716                             bounds[2 * offset + 1] = currh;
717                         }
718                     }
719 
720                     if (j != runLimit) {  // charAt(j) == TAB_CHAR
721                         final float leftX;
722                         final float rightX;
723                         if (runIsRtl) {
724                             rightX = h;
725                             h = mDir * nextTab(h * mDir);
726                             leftX = h;
727                         } else {
728                             leftX = h;
729                             h = mDir * nextTab(h * mDir);
730                             rightX = h;
731                         }
732                         bounds[2 * j] = leftX;
733                         bounds[2 * j + 1] = rightX;
734                         advances[j] = rightX - leftX;
735                     }
736 
737                     segStart = j + 1;
738                 }
739             }
740         }
741     }
742 
743     /**
744      * @see #measure(int, boolean, FontMetricsInt, RectF, LineInfo)
745      * @return The measure results for all possible offsets
746      */
747     @VisibleForTesting
748     public float[] measureAllOffsets(boolean[] trailing, FontMetricsInt fmi) {
749         float[] measurement = new float[mLen + 1];
750         if (trailing[0]) {
751             measurement[0] = 0;
752         }
753 
754         float horizontal = 0;
755         final int runCount = mDirections.getRunCount();
756         for (int runIndex = 0; runIndex < runCount; runIndex++) {
757             final int runStart = mDirections.getRunStart(runIndex);
758             if (runStart > mLen) break;
759             final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
760             final boolean runIsRtl = mDirections.isRunRtl(runIndex);
761             final int runFlag = calculateRunFlag(runIndex, runCount, mDir);
762 
763             int segStart = runStart;
764             for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; ++j) {
765                 if (j == runLimit || charAt(j) == TAB_CHAR) {
766                     final float oldHorizontal = horizontal;
767                     final boolean sameDirection =
768                             (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
769 
770                     // We are using measurement to receive character advance here. So that it
771                     // doesn't need to allocate a new array.
772                     // But be aware that when trailing[segStart] is true, measurement[segStart]
773                     // will be computed in the previous run. And we need to store it first in case
774                     // measureRun overwrites the result.
775                     final float previousSegEndHorizontal = measurement[segStart];
776                     final float width =
777                             measureRun(segStart, j, j, runIsRtl, fmi, null, measurement, segStart,
778                                     0, null, runFlag);
779                     horizontal += sameDirection ? width : -width;
780 
781                     float currHorizontal = sameDirection ? oldHorizontal : horizontal;
782                     final int segLimit = Math.min(j, mLen);
783 
784                     for (int offset = segStart; offset <= segLimit; ++offset) {
785                         float advance = 0f;
786                         // When offset == segLimit, advance is meaningless.
787                         if (offset < segLimit) {
788                             advance = runIsRtl ? -measurement[offset] : measurement[offset];
789                         }
790 
791                         if (offset == segStart && trailing[offset]) {
792                             // If offset == segStart and trailing[segStart] is true, restore the
793                             // value of measurement[segStart] from the previous run.
794                             measurement[offset] = previousSegEndHorizontal;
795                         } else if (offset != segLimit || trailing[offset]) {
796                             measurement[offset] = currHorizontal;
797                         }
798 
799                         currHorizontal += advance;
800                     }
801 
802                     if (j != runLimit) {  // charAt(j) == TAB_CHAR
803                         if (!trailing[j]) {
804                             measurement[j] = horizontal;
805                         }
806                         horizontal = mDir * nextTab(horizontal * mDir);
807                         if (trailing[j + 1]) {
808                             measurement[j + 1] = horizontal;
809                         }
810                     }
811 
812                     segStart = j + 1;
813                 }
814             }
815         }
816         if (!trailing[mLen]) {
817             measurement[mLen] = horizontal;
818         }
819         return measurement;
820     }
821 
822     /**
823      * Draws a unidirectional (but possibly multi-styled) run of text.
824      *
825      *
826      * @param c the canvas to draw on
827      * @param start the line-relative start
828      * @param limit the line-relative limit
829      * @param runIsRtl true if the run is right-to-left
830      * @param x the position of the run that is closest to the leading margin
831      * @param top the top of the line
832      * @param y the baseline
833      * @param bottom the bottom of the line
834      * @param needWidth true if the width value is required.
835      * @param runFlag the run flag to be applied for this run.
836      * @return the signed width of the run, based on the paragraph direction.
837      * Only valid if needWidth is true.
838      */
839     private float drawRun(Canvas c, int start,
840             int limit, boolean runIsRtl, float x, int top, int y, int bottom,
841             boolean needWidth, int runFlag) {
842 
843         if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
844             float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null,
845                     runFlag);
846             handleRun(start, limit, limit, runIsRtl, c, null, x + w, top,
847                     y, bottom, null, null, false, null, 0, null, runFlag);
848             return w;
849         }
850 
851         return handleRun(start, limit, limit, runIsRtl, c, null, x, top,
852                 y, bottom, null, null, needWidth, null, 0, null, runFlag);
853     }
854 
855     /**
856      * Measures a unidirectional (but possibly multi-styled) run of text.
857      *
858      *
859      * @param start the line-relative start of the run
860      * @param offset the offset to measure to, between start and limit inclusive
861      * @param limit the line-relative limit of the run
862      * @param runIsRtl true if the run is right-to-left
863      * @param fmi receives metrics information about the requested
864      * run, can be null.
865      * @param advances receives the advance information about the requested run, can be null.
866      * @param advancesIndex the start index to fill in the advance information.
867      * @param x horizontal offset of the run.
868      * @param lineInfo an optional output parameter for filling line information.
869      * @param runFlag the run flag to be applied for this run.
870      * @return the signed width from the start of the run to the leading edge
871      * of the character at offset, based on the run (not paragraph) direction
872      */
873     private float measureRun(int start, int offset, int limit, boolean runIsRtl,
874             @Nullable FontMetricsInt fmi, @Nullable RectF drawBounds, @Nullable float[] advances,
875             int advancesIndex, float x, @Nullable LineInfo lineInfo, int runFlag) {
876         if (drawBounds != null && (mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
877             float w = -measureRun(start, offset, limit, runIsRtl, null, null, null, 0, 0, null,
878                     runFlag);
879             return handleRun(start, offset, limit, runIsRtl, null, null, x + w, 0, 0, 0, fmi,
880                     drawBounds, true, advances, advancesIndex, lineInfo, runFlag);
881         }
882         return handleRun(start, offset, limit, runIsRtl, null, null, x, 0, 0, 0, fmi, drawBounds,
883                 true, advances, advancesIndex, lineInfo, runFlag);
884     }
885 
886     /**
887      * Shape a unidirectional (but possibly multi-styled) run of text.
888      *
889      * @param consumer the consumer of the shape result
890      * @param start the line-relative start
891      * @param limit the line-relative limit
892      * @param runIsRtl true if the run is right-to-left
893      * @param x the position of the run that is closest to the leading margin
894      * @param needWidth true if the width value is required.
895      * @param runFlag the run flag to be applied for this run.
896      * @return the signed width of the run, based on the paragraph direction.
897      * Only valid if needWidth is true.
898      */
899     private float shapeRun(TextShaper.GlyphsConsumer consumer, int start,
900             int limit, boolean runIsRtl, float x, boolean needWidth, int runFlag) {
901 
902         if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
903             float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null,
904                     runFlag);
905             handleRun(start, limit, limit, runIsRtl, null, consumer, x + w, 0, 0, 0, null, null,
906                     false, null, 0, null, runFlag);
907             return w;
908         }
909 
910         return handleRun(start, limit, limit, runIsRtl, null, consumer, x, 0, 0, 0, null, null,
911                 needWidth, null, 0, null, runFlag);
912     }
913 
914 
915     /**
916      * Walk the cursor through this line, skipping conjuncts and
917      * zero-width characters.
918      *
919      * <p>This function cannot properly walk the cursor off the ends of the line
920      * since it does not know about any shaping on the previous/following line
921      * that might affect the cursor position. Callers must either avoid these
922      * situations or handle the result specially.
923      *
924      * @param cursor the starting position of the cursor, between 0 and the
925      * length of the line, inclusive
926      * @param toLeft true if the caret is moving to the left.
927      * @return the new offset.  If it is less than 0 or greater than the length
928      * of the line, the previous/following line should be examined to get the
929      * actual offset.
930      */
931     int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
932         // 1) The caret marks the leading edge of a character. The character
933         // logically before it might be on a different level, and the active caret
934         // position is on the character at the lower level. If that character
935         // was the previous character, the caret is on its trailing edge.
936         // 2) Take this character/edge and move it in the indicated direction.
937         // This gives you a new character and a new edge.
938         // 3) This position is between two visually adjacent characters.  One of
939         // these might be at a lower level.  The active position is on the
940         // character at the lower level.
941         // 4) If the active position is on the trailing edge of the character,
942         // the new caret position is the following logical character, else it
943         // is the character.
944 
945         int lineStart = 0;
946         int lineEnd = mLen;
947         boolean paraIsRtl = mDir == -1;
948         int[] runs = mDirections.mDirections;
949 
950         int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
951         boolean trailing = false;
952 
953         if (cursor == lineStart) {
954             runIndex = -2;
955         } else if (cursor == lineEnd) {
956             runIndex = runs.length;
957         } else {
958           // First, get information about the run containing the character with
959           // the active caret.
960           for (runIndex = 0; runIndex < runs.length; runIndex += 2) {
961             runStart = lineStart + runs[runIndex];
962             if (cursor >= runStart) {
963               runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK);
964               if (runLimit > lineEnd) {
965                   runLimit = lineEnd;
966               }
967               if (cursor < runLimit) {
968                 runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
969                     Layout.RUN_LEVEL_MASK;
970                 if (cursor == runStart) {
971                   // The caret is on a run boundary, see if we should
972                   // use the position on the trailing edge of the previous
973                   // logical character instead.
974                   int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;
975                   int pos = cursor - 1;
976                   for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {
977                     prevRunStart = lineStart + runs[prevRunIndex];
978                     if (pos >= prevRunStart) {
979                       prevRunLimit = prevRunStart +
980                           (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK);
981                       if (prevRunLimit > lineEnd) {
982                           prevRunLimit = lineEnd;
983                       }
984                       if (pos < prevRunLimit) {
985                         prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT)
986                             & Layout.RUN_LEVEL_MASK;
987                         if (prevRunLevel < runLevel) {
988                           // Start from logically previous character.
989                           runIndex = prevRunIndex;
990                           runLevel = prevRunLevel;
991                           runStart = prevRunStart;
992                           runLimit = prevRunLimit;
993                           trailing = true;
994                           break;
995                         }
996                       }
997                     }
998                   }
999                 }
1000                 break;
1001               }
1002             }
1003           }
1004 
1005           // caret might be == lineEnd.  This is generally a space or paragraph
1006           // separator and has an associated run, but might be the end of
1007           // text, in which case it doesn't.  If that happens, we ran off the
1008           // end of the run list, and runIndex == runs.length.  In this case,
1009           // we are at a run boundary so we skip the below test.
1010           if (runIndex != runs.length) {
1011               boolean runIsRtl = (runLevel & 0x1) != 0;
1012               boolean advance = toLeft == runIsRtl;
1013               if (cursor != (advance ? runLimit : runStart) || advance != trailing) {
1014                   // Moving within or into the run, so we can move logically.
1015                   newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit,
1016                           runIsRtl, cursor, advance);
1017                   // If the new position is internal to the run, we're at the strong
1018                   // position already so we're finished.
1019                   if (newCaret != (advance ? runLimit : runStart)) {
1020                       return newCaret;
1021                   }
1022               }
1023           }
1024         }
1025 
1026         // If newCaret is -1, we're starting at a run boundary and crossing
1027         // into another run. Otherwise we've arrived at a run boundary, and
1028         // need to figure out which character to attach to.  Note we might
1029         // need to run this twice, if we cross a run boundary and end up at
1030         // another run boundary.
1031         while (true) {
1032           boolean advance = toLeft == paraIsRtl;
1033           int otherRunIndex = runIndex + (advance ? 2 : -2);
1034           if (otherRunIndex >= 0 && otherRunIndex < runs.length) {
1035             int otherRunStart = lineStart + runs[otherRunIndex];
1036             int otherRunLimit = otherRunStart +
1037             (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK);
1038             if (otherRunLimit > lineEnd) {
1039                 otherRunLimit = lineEnd;
1040             }
1041             int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
1042                 Layout.RUN_LEVEL_MASK;
1043             boolean otherRunIsRtl = (otherRunLevel & 1) != 0;
1044 
1045             advance = toLeft == otherRunIsRtl;
1046             if (newCaret == -1) {
1047                 newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart,
1048                         otherRunLimit, otherRunIsRtl,
1049                         advance ? otherRunStart : otherRunLimit, advance);
1050                 if (newCaret == (advance ? otherRunLimit : otherRunStart)) {
1051                     // Crossed and ended up at a new boundary,
1052                     // repeat a second and final time.
1053                     runIndex = otherRunIndex;
1054                     runLevel = otherRunLevel;
1055                     continue;
1056                 }
1057                 break;
1058             }
1059 
1060             // The new caret is at a boundary.
1061             if (otherRunLevel < runLevel) {
1062               // The strong character is in the other run.
1063               newCaret = advance ? otherRunStart : otherRunLimit;
1064             }
1065             break;
1066           }
1067 
1068           if (newCaret == -1) {
1069               // We're walking off the end of the line.  The paragraph
1070               // level is always equal to or lower than any internal level, so
1071               // the boundaries get the strong caret.
1072               newCaret = advance ? mLen + 1 : -1;
1073               break;
1074           }
1075 
1076           // Else we've arrived at the end of the line.  That's a strong position.
1077           // We might have arrived here by crossing over a run with no internal
1078           // breaks and dropping out of the above loop before advancing one final
1079           // time, so reset the caret.
1080           // Note, we use '<=' below to handle a situation where the only run
1081           // on the line is a counter-directional run.  If we're not advancing,
1082           // we can end up at the 'lineEnd' position but the caret we want is at
1083           // the lineStart.
1084           if (newCaret <= lineEnd) {
1085               newCaret = advance ? lineEnd : lineStart;
1086           }
1087           break;
1088         }
1089 
1090         return newCaret;
1091     }
1092 
1093     /**
1094      * Returns the next valid offset within this directional run, skipping
1095      * conjuncts and zero-width characters.  This should not be called to walk
1096      * off the end of the line, since the returned values might not be valid
1097      * on neighboring lines.  If the returned offset is less than zero or
1098      * greater than the line length, the offset should be recomputed on the
1099      * preceding or following line, respectively.
1100      *
1101      * @param runIndex the run index
1102      * @param runStart the start of the run
1103      * @param runLimit the limit of the run
1104      * @param runIsRtl true if the run is right-to-left
1105      * @param offset the offset
1106      * @param after true if the new offset should logically follow the provided
1107      * offset
1108      * @return the new offset
1109      */
getOffsetBeforeAfter(int runIndex, int runStart, int runLimit, boolean runIsRtl, int offset, boolean after)1110     private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit,
1111             boolean runIsRtl, int offset, boolean after) {
1112 
1113         if (runIndex < 0 || offset == (after ? mLen : 0)) {
1114             // Walking off end of line.  Since we don't know
1115             // what cursor positions are available on other lines, we can't
1116             // return accurate values.  These are a guess.
1117             if (after) {
1118                 return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;
1119             }
1120             return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;
1121         }
1122 
1123         TextPaint wp = mWorkPaint;
1124         wp.set(mPaint);
1125         if (mIsJustifying) {
1126             wp.setWordSpacing(mAddedWordSpacingInPx);
1127             wp.setLetterSpacing(mAddedLetterSpacingInPx / wp.getTextSize());  // Convert to Em
1128         }
1129 
1130         int spanStart = runStart;
1131         int spanLimit;
1132         if (mSpanned == null || runStart == runLimit) {
1133             spanLimit = runLimit;
1134         } else {
1135             int target = after ? offset + 1 : offset;
1136             int limit = mStart + runLimit;
1137             while (true) {
1138                 spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,
1139                         MetricAffectingSpan.class) - mStart;
1140                 if (spanLimit >= target) {
1141                     break;
1142                 }
1143                 spanStart = spanLimit;
1144             }
1145 
1146             MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,
1147                     mStart + spanLimit, MetricAffectingSpan.class);
1148             spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);
1149 
1150             if (spans.length > 0) {
1151                 ReplacementSpan replacement = null;
1152                 for (int j = 0; j < spans.length; j++) {
1153                     MetricAffectingSpan span = spans[j];
1154                     if (span instanceof ReplacementSpan) {
1155                         replacement = (ReplacementSpan)span;
1156                     } else {
1157                         span.updateMeasureState(wp);
1158                     }
1159                 }
1160 
1161                 if (replacement != null) {
1162                     // If we have a replacement span, we're moving either to
1163                     // the start or end of this span.
1164                     return after ? spanLimit : spanStart;
1165                 }
1166             }
1167         }
1168 
1169         int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;
1170         if (mCharsValid) {
1171             return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,
1172                     runIsRtl, offset, cursorOpt);
1173         } else {
1174             return wp.getTextRunCursor(mText, mStart + spanStart,
1175                     mStart + spanLimit, runIsRtl, mStart + offset, cursorOpt) - mStart;
1176         }
1177     }
1178 
1179     /**
1180      * @param wp
1181      */
expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp)1182     private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
1183         final int previousTop     = fmi.top;
1184         final int previousAscent  = fmi.ascent;
1185         final int previousDescent = fmi.descent;
1186         final int previousBottom  = fmi.bottom;
1187         final int previousLeading = fmi.leading;
1188 
1189         wp.getFontMetricsInt(fmi);
1190 
1191         updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
1192                 previousLeading);
1193     }
1194 
expandMetricsFromPaint(TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, FontMetricsInt fmi)1195     private void expandMetricsFromPaint(TextPaint wp, int start, int end,
1196             int contextStart, int contextEnd, boolean runIsRtl, FontMetricsInt fmi) {
1197 
1198         final int previousTop     = fmi.top;
1199         final int previousAscent  = fmi.ascent;
1200         final int previousDescent = fmi.descent;
1201         final int previousBottom  = fmi.bottom;
1202         final int previousLeading = fmi.leading;
1203 
1204         int count = end - start;
1205         int contextCount = contextEnd - contextStart;
1206         if (mCharsValid) {
1207             wp.getFontMetricsInt(mChars, start, count, contextStart, contextCount, runIsRtl,
1208                     fmi);
1209         } else {
1210             if (mComputed == null) {
1211                 wp.getFontMetricsInt(mText, mStart + start, count, mStart + contextStart,
1212                         contextCount, runIsRtl, fmi);
1213             } else {
1214                 mComputed.getFontMetricsInt(mStart + start, mStart + end, fmi);
1215             }
1216         }
1217 
1218         updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
1219                 previousLeading);
1220     }
1221 
1222 
updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent, int previousDescent, int previousBottom, int previousLeading)1223     static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
1224             int previousDescent, int previousBottom, int previousLeading) {
1225         fmi.top     = Math.min(fmi.top,     previousTop);
1226         fmi.ascent  = Math.min(fmi.ascent,  previousAscent);
1227         fmi.descent = Math.max(fmi.descent, previousDescent);
1228         fmi.bottom  = Math.max(fmi.bottom,  previousBottom);
1229         fmi.leading = Math.max(fmi.leading, previousLeading);
1230     }
1231 
drawStroke(TextPaint wp, Canvas c, int color, float position, float thickness, float xleft, float xright, float baseline)1232     private static void drawStroke(TextPaint wp, Canvas c, int color, float position,
1233             float thickness, float xleft, float xright, float baseline) {
1234         final float strokeTop = baseline + wp.baselineShift + position;
1235 
1236         final int previousColor = wp.getColor();
1237         final Paint.Style previousStyle = wp.getStyle();
1238         final boolean previousAntiAlias = wp.isAntiAlias();
1239 
1240         wp.setStyle(Paint.Style.FILL);
1241         wp.setAntiAlias(true);
1242 
1243         wp.setColor(color);
1244         c.drawRect(xleft, strokeTop, xright, strokeTop + thickness, wp);
1245 
1246         wp.setStyle(previousStyle);
1247         wp.setColor(previousColor);
1248         wp.setAntiAlias(previousAntiAlias);
1249     }
1250 
getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, int offset, @Nullable float[] advances, int advancesIndex, RectF drawingBounds, @Nullable LineInfo lineInfo)1251     private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
1252             boolean runIsRtl, int offset, @Nullable float[] advances, int advancesIndex,
1253             RectF drawingBounds, @Nullable LineInfo lineInfo) {
1254         if (lineInfo != null) {
1255             if (mRunInfo == null) {
1256                 mRunInfo = new Paint.RunInfo();
1257             }
1258             mRunInfo.setClusterCount(0);
1259         } else {
1260             mRunInfo = null;
1261         }
1262         if (mCharsValid) {
1263             float r = wp.getRunCharacterAdvance(mChars, start, end, contextStart, contextEnd,
1264                     runIsRtl, offset, advances, advancesIndex, drawingBounds, mRunInfo);
1265             if (lineInfo != null) {
1266                 lineInfo.setClusterCount(lineInfo.getClusterCount() + mRunInfo.getClusterCount());
1267             }
1268             return r;
1269         } else {
1270             final int delta = mStart;
1271             // TODO: Add cluster information to the PrecomputedText for better performance of
1272             // justification.
1273             if (mComputed == null || advances != null || lineInfo != null) {
1274                 float r = wp.getRunCharacterAdvance(mText, delta + start, delta + end,
1275                         delta + contextStart, delta + contextEnd, runIsRtl,
1276                         delta + offset, advances, advancesIndex, drawingBounds, mRunInfo);
1277                 if (lineInfo != null) {
1278                     lineInfo.setClusterCount(
1279                             lineInfo.getClusterCount() + mRunInfo.getClusterCount());
1280                 }
1281                 return r;
1282             } else {
1283                 if (drawingBounds != null) {
1284                     if (mTmpRectForPrecompute == null) {
1285                         mTmpRectForPrecompute = new Rect();
1286                     }
1287                     mComputed.getBounds(start + delta, end + delta, mTmpRectForPrecompute);
1288                     drawingBounds.set(mTmpRectForPrecompute);
1289                 }
1290                 return mComputed.getWidth(start + delta, end + delta);
1291             }
1292         }
1293     }
1294 
1295     /**
1296      * Utility function for measuring and rendering text.  The text must
1297      * not include a tab.
1298      *
1299      * @param wp the working paint
1300      * @param start the start of the text
1301      * @param end the end of the text
1302      * @param runIsRtl true if the run is right-to-left
1303      * @param c the canvas, can be null if rendering is not needed
1304      * @param consumer the output positioned glyph list, can be null if not necessary
1305      * @param x the edge of the run closest to the leading margin
1306      * @param top the top of the line
1307      * @param y the baseline
1308      * @param bottom the bottom of the line
1309      * @param fmi receives metrics information, can be null
1310      * @param needWidth true if the width of the run is needed
1311      * @param offset the offset for the purpose of measuring
1312      * @param decorations the list of locations and paremeters for drawing decorations
1313      * @param advances receives the advance information about the requested run, can be null.
1314      * @param advancesIndex the start index to fill in the advance information.
1315      * @param lineInfo an optional output parameter for filling line information.
1316      * @param runFlag the run flag to be applied for this run.
1317      * @return the signed width of the run based on the run direction; only
1318      * valid if needWidth is true
1319      */
handleText(TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, Canvas c, TextShaper.GlyphsConsumer consumer, float x, int top, int y, int bottom, FontMetricsInt fmi, RectF drawBounds, boolean needWidth, int offset, @Nullable ArrayList<DecorationInfo> decorations, @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo, int runFlag)1320     private float handleText(TextPaint wp, int start, int end,
1321             int contextStart, int contextEnd, boolean runIsRtl,
1322             Canvas c, TextShaper.GlyphsConsumer consumer, float x, int top, int y, int bottom,
1323             FontMetricsInt fmi, RectF drawBounds, boolean needWidth, int offset,
1324             @Nullable ArrayList<DecorationInfo> decorations,
1325             @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo,
1326             int runFlag) {
1327         if (mIsJustifying) {
1328             wp.setWordSpacing(mAddedWordSpacingInPx);
1329             wp.setLetterSpacing(mAddedLetterSpacingInPx / wp.getTextSize());  // Convert to Em
1330         }
1331         // Get metrics first (even for empty strings or "0" width runs)
1332         if (drawBounds != null && fmi == null) {
1333             fmi = new FontMetricsInt();
1334         }
1335         if (fmi != null) {
1336             expandMetricsFromPaint(fmi, wp);
1337         }
1338 
1339         // No need to do anything if the run width is "0"
1340         if (end == start) {
1341             return 0f;
1342         }
1343 
1344         float totalWidth = 0;
1345         if ((runFlag & Paint.TEXT_RUN_FLAG_LEFT_EDGE) == Paint.TEXT_RUN_FLAG_LEFT_EDGE) {
1346             wp.setFlags(wp.getFlags() | Paint.TEXT_RUN_FLAG_LEFT_EDGE);
1347         } else {
1348             wp.setFlags(wp.getFlags() & ~Paint.TEXT_RUN_FLAG_LEFT_EDGE);
1349         }
1350         if ((runFlag & Paint.TEXT_RUN_FLAG_RIGHT_EDGE) == Paint.TEXT_RUN_FLAG_RIGHT_EDGE) {
1351             wp.setFlags(wp.getFlags() | Paint.TEXT_RUN_FLAG_RIGHT_EDGE);
1352         } else {
1353             wp.setFlags(wp.getFlags() & ~Paint.TEXT_RUN_FLAG_RIGHT_EDGE);
1354         }
1355         final int numDecorations = decorations == null ? 0 : decorations.size();
1356         if (needWidth || ((c != null || consumer != null) && (wp.bgColor != 0
1357                 || numDecorations != 0 || runIsRtl))) {
1358             if (drawBounds != null && mTmpRectForPaintAPI == null) {
1359                 mTmpRectForPaintAPI = new RectF();
1360             }
1361             totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset,
1362                     advances, advancesIndex, drawBounds == null ? null : mTmpRectForPaintAPI,
1363                     lineInfo);
1364             if (drawBounds != null) {
1365                 if (runIsRtl) {
1366                     mTmpRectForPaintAPI.offset(x - totalWidth, 0);
1367                 } else {
1368                     mTmpRectForPaintAPI.offset(x, 0);
1369                 }
1370                 drawBounds.union(mTmpRectForPaintAPI);
1371             }
1372         }
1373 
1374         final float leftX, rightX;
1375         if (runIsRtl) {
1376             leftX = x - totalWidth;
1377             rightX = x;
1378         } else {
1379             leftX = x;
1380             rightX = x + totalWidth;
1381         }
1382 
1383         if (consumer != null) {
1384             shapeTextRun(consumer, wp, start, end, contextStart, contextEnd, runIsRtl, leftX);
1385         }
1386 
1387         if (mUseFallbackExtent && fmi != null) {
1388             expandMetricsFromPaint(wp, start, end, contextStart, contextEnd, runIsRtl, fmi);
1389         }
1390 
1391         if (c != null) {
1392             if (wp.bgColor != 0) {
1393                 int previousColor = wp.getColor();
1394                 Paint.Style previousStyle = wp.getStyle();
1395 
1396                 wp.setColor(wp.bgColor);
1397                 wp.setStyle(Paint.Style.FILL);
1398                 c.drawRect(leftX, top, rightX, bottom, wp);
1399 
1400                 wp.setStyle(previousStyle);
1401                 wp.setColor(previousColor);
1402             }
1403 
1404             drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
1405                     leftX, y + wp.baselineShift);
1406 
1407             if (numDecorations != 0) {
1408                 for (int i = 0; i < numDecorations; i++) {
1409                     final DecorationInfo info = decorations.get(i);
1410 
1411                     final int decorationStart = Math.max(info.start, start);
1412                     final int decorationEnd = Math.min(info.end, offset);
1413                     float decorationStartAdvance = getRunAdvance(wp, start, end, contextStart,
1414                             contextEnd, runIsRtl, decorationStart, null, 0, null, null);
1415                     float decorationEndAdvance = getRunAdvance(wp, start, end, contextStart,
1416                             contextEnd, runIsRtl, decorationEnd, null, 0, null, null);
1417                     final float decorationXLeft, decorationXRight;
1418                     if (runIsRtl) {
1419                         decorationXLeft = rightX - decorationEndAdvance;
1420                         decorationXRight = rightX - decorationStartAdvance;
1421                     } else {
1422                         decorationXLeft = leftX + decorationStartAdvance;
1423                         decorationXRight = leftX + decorationEndAdvance;
1424                     }
1425 
1426                     // Theoretically, there could be cases where both Paint's and TextPaint's
1427                     // setUnderLineText() are called. For backward compatibility, we need to draw
1428                     // both underlines, the one with custom color first.
1429                     if (info.underlineColor != 0) {
1430                         drawStroke(wp, c, info.underlineColor, wp.getUnderlinePosition(),
1431                                 info.underlineThickness, decorationXLeft, decorationXRight, y);
1432                     }
1433                     if (info.isUnderlineText) {
1434                         final float thickness =
1435                                 Math.max(wp.getUnderlineThickness(), 1.0f);
1436                         drawStroke(wp, c, wp.getColor(), wp.getUnderlinePosition(), thickness,
1437                                 decorationXLeft, decorationXRight, y);
1438                     }
1439 
1440                     if (info.isStrikeThruText) {
1441                         final float thickness =
1442                                 Math.max(wp.getStrikeThruThickness(), 1.0f);
1443                         drawStroke(wp, c, wp.getColor(), wp.getStrikeThruPosition(), thickness,
1444                                 decorationXLeft, decorationXRight, y);
1445                     }
1446                 }
1447             }
1448 
1449         }
1450 
1451         return runIsRtl ? -totalWidth : totalWidth;
1452     }
1453 
1454     /**
1455      * Utility function for measuring and rendering a replacement.
1456      *
1457      *
1458      * @param replacement the replacement
1459      * @param wp the work paint
1460      * @param start the start of the run
1461      * @param limit the limit of the run
1462      * @param runIsRtl true if the run is right-to-left
1463      * @param c the canvas, can be null if not rendering
1464      * @param x the edge of the replacement closest to the leading margin
1465      * @param top the top of the line
1466      * @param y the baseline
1467      * @param bottom the bottom of the line
1468      * @param fmi receives metrics information, can be null
1469      * @param needWidth true if the width of the replacement is needed
1470      * @return the signed width of the run based on the run direction; only
1471      * valid if needWidth is true
1472      */
handleReplacement(ReplacementSpan replacement, TextPaint wp, int start, int limit, boolean runIsRtl, Canvas c, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth)1473     private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
1474             int start, int limit, boolean runIsRtl, Canvas c,
1475             float x, int top, int y, int bottom, FontMetricsInt fmi,
1476             boolean needWidth) {
1477 
1478         float ret = 0;
1479 
1480         int textStart = mStart + start;
1481         int textLimit = mStart + limit;
1482 
1483         if (needWidth || (c != null && runIsRtl)) {
1484             int previousTop = 0;
1485             int previousAscent = 0;
1486             int previousDescent = 0;
1487             int previousBottom = 0;
1488             int previousLeading = 0;
1489 
1490             boolean needUpdateMetrics = (fmi != null);
1491 
1492             if (needUpdateMetrics) {
1493                 previousTop     = fmi.top;
1494                 previousAscent  = fmi.ascent;
1495                 previousDescent = fmi.descent;
1496                 previousBottom  = fmi.bottom;
1497                 previousLeading = fmi.leading;
1498             }
1499 
1500             ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
1501 
1502             if (needUpdateMetrics) {
1503                 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
1504                         previousLeading);
1505             }
1506         }
1507 
1508         if (c != null) {
1509             if (runIsRtl) {
1510                 x -= ret;
1511             }
1512             replacement.draw(c, mText, textStart, textLimit,
1513                     x, top, y, bottom, wp);
1514         }
1515 
1516         return runIsRtl ? -ret : ret;
1517     }
1518 
adjustStartHyphenEdit(int start, @Paint.StartHyphenEdit int startHyphenEdit)1519     private int adjustStartHyphenEdit(int start, @Paint.StartHyphenEdit int startHyphenEdit) {
1520         // Only draw hyphens on first in line. Disable them otherwise.
1521         return start > 0 ? Paint.START_HYPHEN_EDIT_NO_EDIT : startHyphenEdit;
1522     }
1523 
adjustEndHyphenEdit(int limit, @Paint.EndHyphenEdit int endHyphenEdit)1524     private int adjustEndHyphenEdit(int limit, @Paint.EndHyphenEdit int endHyphenEdit) {
1525         // Only draw hyphens on last run in line. Disable them otherwise.
1526         return limit < mLen ? Paint.END_HYPHEN_EDIT_NO_EDIT : endHyphenEdit;
1527     }
1528 
1529     private static final class DecorationInfo {
1530         public boolean isStrikeThruText;
1531         public boolean isUnderlineText;
1532         public int underlineColor;
1533         public float underlineThickness;
1534         public int start = -1;
1535         public int end = -1;
1536 
hasDecoration()1537         public boolean hasDecoration() {
1538             return isStrikeThruText || isUnderlineText || underlineColor != 0;
1539         }
1540 
1541         // Copies the info, but not the start and end range.
copyInfo()1542         public DecorationInfo copyInfo() {
1543             final DecorationInfo copy = new DecorationInfo();
1544             copy.isStrikeThruText = isStrikeThruText;
1545             copy.isUnderlineText = isUnderlineText;
1546             copy.underlineColor = underlineColor;
1547             copy.underlineThickness = underlineThickness;
1548             return copy;
1549         }
1550     }
1551 
extractDecorationInfo(@onNull TextPaint paint, @NonNull DecorationInfo info)1552     private void extractDecorationInfo(@NonNull TextPaint paint, @NonNull DecorationInfo info) {
1553         info.isStrikeThruText = paint.isStrikeThruText();
1554         if (info.isStrikeThruText) {
1555             paint.setStrikeThruText(false);
1556         }
1557         info.isUnderlineText = paint.isUnderlineText();
1558         if (info.isUnderlineText) {
1559             paint.setUnderlineText(false);
1560         }
1561         info.underlineColor = paint.underlineColor;
1562         info.underlineThickness = paint.underlineThickness;
1563         paint.setUnderlineText(0, 0.0f);
1564     }
1565 
1566     /**
1567      * Utility function for handling a unidirectional run.  The run must not
1568      * contain tabs but can contain styles.
1569      *
1570      *
1571      * @param start the line-relative start of the run
1572      * @param measureLimit the offset to measure to, between start and limit inclusive
1573      * @param limit the limit of the run
1574      * @param runIsRtl true if the run is right-to-left
1575      * @param c the canvas, can be null
1576      * @param consumer the output positioned glyphs, can be null
1577      * @param x the end of the run closest to the leading margin
1578      * @param top the top of the line
1579      * @param y the baseline
1580      * @param bottom the bottom of the line
1581      * @param fmi receives metrics information, can be null
1582      * @param needWidth true if the width is required
1583      * @param advances receives the advance information about the requested run, can be null.
1584      * @param advancesIndex the start index to fill in the advance information.
1585      * @param lineInfo an optional output parameter for filling line information.
1586      * @param runFlag the run flag to be applied for this run.
1587      * @return the signed width of the run based on the run direction; only
1588      * valid if needWidth is true
1589      */
handleRun(int start, int measureLimit, int limit, boolean runIsRtl, Canvas c, TextShaper.GlyphsConsumer consumer, float x, int top, int y, int bottom, FontMetricsInt fmi, RectF drawBounds, boolean needWidth, @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo, int runFlag)1590     private float handleRun(int start, int measureLimit,
1591             int limit, boolean runIsRtl, Canvas c,
1592             TextShaper.GlyphsConsumer consumer, float x, int top, int y,
1593             int bottom, FontMetricsInt fmi, RectF drawBounds, boolean needWidth,
1594             @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo,
1595             int runFlag) {
1596 
1597         if (measureLimit < start || measureLimit > limit) {
1598             throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
1599                     + "start (" + start + ") and limit (" + limit + ") bounds");
1600         }
1601 
1602         if (advances != null && advances.length - advancesIndex < measureLimit - start) {
1603             throw new IndexOutOfBoundsException("advances doesn't have enough space to receive the "
1604                     + "result");
1605         }
1606 
1607         // Case of an empty line, make sure we update fmi according to mPaint
1608         if (start == measureLimit) {
1609             final TextPaint wp = mWorkPaint;
1610             wp.set(mPaint);
1611             if (fmi != null) {
1612                 expandMetricsFromPaint(fmi, wp);
1613             }
1614             if (drawBounds != null) {
1615                 if (fmi == null) {
1616                     FontMetricsInt tmpFmi = new FontMetricsInt();
1617                     expandMetricsFromPaint(tmpFmi, wp);
1618                     fmi = tmpFmi;
1619                 }
1620                 drawBounds.union(0f, fmi.top, 0f, fmi.bottom);
1621             }
1622             return 0f;
1623         }
1624 
1625         final boolean needsSpanMeasurement;
1626         if (mSpanned == null) {
1627             needsSpanMeasurement = false;
1628         } else {
1629             mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
1630             mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
1631             needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0
1632                     || mCharacterStyleSpanSet.numberOfSpans != 0;
1633         }
1634 
1635         if (!needsSpanMeasurement) {
1636             final TextPaint wp = mWorkPaint;
1637             wp.set(mPaint);
1638             wp.setStartHyphenEdit(adjustStartHyphenEdit(start, wp.getStartHyphenEdit()));
1639             wp.setEndHyphenEdit(adjustEndHyphenEdit(limit, wp.getEndHyphenEdit()));
1640             return handleText(wp, start, limit, start, limit, runIsRtl, c, consumer, x, top,
1641                     y, bottom, fmi, drawBounds, needWidth, measureLimit, null, advances,
1642                     advancesIndex, lineInfo, runFlag);
1643         }
1644 
1645         // Shaping needs to take into account context up to metric boundaries,
1646         // but rendering needs to take into account character style boundaries.
1647         // So we iterate through metric runs to get metric bounds,
1648         // then within each metric run iterate through character style runs
1649         // for the run bounds.
1650         final float originalX = x;
1651         for (int i = start, inext; i < measureLimit; i = inext) {
1652             final TextPaint wp = mWorkPaint;
1653             wp.set(mPaint);
1654 
1655             inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
1656                     mStart;
1657             int mlimit = Math.min(inext, measureLimit);
1658 
1659             ReplacementSpan replacement = null;
1660 
1661             for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
1662                 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
1663                 // empty by construction. This special case in getSpans() explains the >= & <= tests
1664                 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit)
1665                         || (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
1666 
1667                 boolean insideEllipsis =
1668                         mStart + mEllipsisStart <= mMetricAffectingSpanSpanSet.spanStarts[j]
1669                         && mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + mEllipsisEnd;
1670                 final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
1671                 if (span instanceof ReplacementSpan) {
1672                     replacement = !insideEllipsis ? (ReplacementSpan) span : null;
1673                 } else {
1674                     // We might have a replacement that uses the draw
1675                     // state, otherwise measure state would suffice.
1676                     span.updateDrawState(wp);
1677                 }
1678             }
1679 
1680             if (replacement != null) {
1681                 final float width = handleReplacement(replacement, wp, i, mlimit, runIsRtl, c,
1682                         x, top, y, bottom, fmi, needWidth || mlimit < measureLimit);
1683                 x += width;
1684                 if (advances != null) {
1685                     // For replacement, the entire width is assigned to the first character.
1686                     advances[advancesIndex + i - start] = runIsRtl ? -width : width;
1687                     for (int j = i + 1; j < mlimit; ++j) {
1688                         advances[advancesIndex + j - start] = 0.0f;
1689                     }
1690                 }
1691                 continue;
1692             }
1693 
1694             final TextPaint activePaint = mActivePaint;
1695             activePaint.set(mPaint);
1696             int activeStart = i;
1697             int activeEnd = mlimit;
1698             final DecorationInfo decorationInfo = mDecorationInfo;
1699             mDecorations.clear();
1700             for (int j = i, jnext; j < mlimit; j = jnext) {
1701                 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
1702                         mStart;
1703 
1704                 final int offset = Math.min(jnext, mlimit);
1705                 wp.set(mPaint);
1706                 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
1707                     // Intentionally using >= and <= as explained above
1708                     if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
1709                             (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
1710 
1711                     final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
1712                     span.updateDrawState(wp);
1713                 }
1714 
1715                 extractDecorationInfo(wp, decorationInfo);
1716 
1717                 if (j == i) {
1718                     // First chunk of text. We can't handle it yet, since we may need to merge it
1719                     // with the next chunk. So we just save the TextPaint for future comparisons
1720                     // and use.
1721                     activePaint.set(wp);
1722                 } else if (!equalAttributes(wp, activePaint)) {
1723                     final int spanRunFlag = resolveRunFlagForSubSequence(
1724                             runFlag, runIsRtl, start, measureLimit, activeStart, activeEnd);
1725 
1726                     // The style of the present chunk of text is substantially different from the
1727                     // style of the previous chunk. We need to handle the active piece of text
1728                     // and restart with the present chunk.
1729                     activePaint.setStartHyphenEdit(
1730                             adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit()));
1731                     activePaint.setEndHyphenEdit(
1732                             adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit()));
1733                     x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c,
1734                             consumer, x, top, y, bottom, fmi, drawBounds,
1735                             needWidth || activeEnd < measureLimit,
1736                             Math.min(activeEnd, mlimit), mDecorations,
1737                             advances, advancesIndex + activeStart - start, lineInfo, spanRunFlag);
1738 
1739                     activeStart = j;
1740                     activePaint.set(wp);
1741                     mDecorations.clear();
1742                 } else {
1743                     // The present TextPaint is substantially equal to the last TextPaint except
1744                     // perhaps for decorations. We just need to expand the active piece of text to
1745                     // include the present chunk, which we always do anyway. We don't need to save
1746                     // wp to activePaint, since they are already equal.
1747                 }
1748 
1749                 activeEnd = jnext;
1750                 if (decorationInfo.hasDecoration()) {
1751                     final DecorationInfo copy = decorationInfo.copyInfo();
1752                     copy.start = j;
1753                     copy.end = jnext;
1754                     mDecorations.add(copy);
1755                 }
1756             }
1757 
1758             final int spanRunFlag = resolveRunFlagForSubSequence(
1759                     runFlag, runIsRtl, start, measureLimit, activeStart, activeEnd);
1760             // Handle the final piece of text.
1761             activePaint.setStartHyphenEdit(
1762                     adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit()));
1763             activePaint.setEndHyphenEdit(
1764                     adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit()));
1765             x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, consumer, x,
1766                     top, y, bottom, fmi, drawBounds, needWidth || activeEnd < measureLimit,
1767                     Math.min(activeEnd, mlimit), mDecorations,
1768                     advances, advancesIndex + activeStart - start, lineInfo, spanRunFlag);
1769         }
1770 
1771         return x - originalX;
1772     }
1773 
1774     /**
1775      * Render a text run with the set-up paint.
1776      *
1777      * @param c the canvas
1778      * @param wp the paint used to render the text
1779      * @param start the start of the run
1780      * @param end the end of the run
1781      * @param contextStart the start of context for the run
1782      * @param contextEnd the end of the context for the run
1783      * @param runIsRtl true if the run is right-to-left
1784      * @param x the x position of the left edge of the run
1785      * @param y the baseline of the run
1786      */
drawTextRun(Canvas c, TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x, int y)1787     private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
1788             int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
1789         if (mCharsValid) {
1790             int count = end - start;
1791             int contextCount = contextEnd - contextStart;
1792             c.drawTextRun(mChars, start, count, contextStart, contextCount,
1793                     x, y, runIsRtl, wp);
1794         } else {
1795             int delta = mStart;
1796             c.drawTextRun(mText, delta + start, delta + end,
1797                     delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
1798         }
1799     }
1800 
1801     /**
1802      * Shape a text run with the set-up paint.
1803      *
1804      * @param consumer the output positioned glyphs list
1805      * @param paint the paint used to render the text
1806      * @param start the start of the run
1807      * @param end the end of the run
1808      * @param contextStart the start of context for the run
1809      * @param contextEnd the end of the context for the run
1810      * @param runIsRtl true if the run is right-to-left
1811      * @param x the x position of the left edge of the run
1812      */
shapeTextRun(TextShaper.GlyphsConsumer consumer, TextPaint paint, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x)1813     private void shapeTextRun(TextShaper.GlyphsConsumer consumer, TextPaint paint,
1814             int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x) {
1815 
1816         int count = end - start;
1817         int contextCount = contextEnd - contextStart;
1818         PositionedGlyphs glyphs;
1819         if (mCharsValid) {
1820             glyphs = TextRunShaper.shapeTextRun(
1821                     mChars,
1822                     start, count,
1823                     contextStart, contextCount,
1824                     x, 0f,
1825                     runIsRtl,
1826                     paint
1827             );
1828         } else {
1829             glyphs = TextRunShaper.shapeTextRun(
1830                     mText,
1831                     mStart + start, count,
1832                     mStart + contextStart, contextCount,
1833                     x, 0f,
1834                     runIsRtl,
1835                     paint
1836             );
1837         }
1838         consumer.accept(start, count, glyphs, paint);
1839     }
1840 
1841 
1842     /**
1843      * Returns the next tab position.
1844      *
1845      * @param h the (unsigned) offset from the leading margin
1846      * @return the (unsigned) tab position after this offset
1847      */
nextTab(float h)1848     float nextTab(float h) {
1849         if (mTabs != null) {
1850             return mTabs.nextTab(h);
1851         }
1852         return TabStops.nextDefaultStop(h, TAB_INCREMENT);
1853     }
1854 
isStretchableWhitespace(int ch)1855     private boolean isStretchableWhitespace(int ch) {
1856         // TODO: Support NBSP and other stretchable whitespace (b/34013491 and b/68204709).
1857         return ch == 0x0020;
1858     }
1859 
1860     /* Return the number of spaces in the text line, for the purpose of justification */
countStretchableSpaces(int start, int end)1861     private int countStretchableSpaces(int start, int end) {
1862         int count = 0;
1863         for (int i = start; i < end; i++) {
1864             final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart);
1865             if (isStretchableWhitespace(c)) {
1866                 count++;
1867             }
1868         }
1869         return count;
1870     }
1871 
1872     // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
isLineEndSpace(char ch)1873     public static boolean isLineEndSpace(char ch) {
1874         return ch == ' ' || ch == '\t' || ch == 0x1680
1875                 || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007)
1876                 || ch == 0x205F || ch == 0x3000;
1877     }
1878 
1879     private static final int TAB_INCREMENT = 20;
1880 
equalAttributes(@onNull TextPaint lp, @NonNull TextPaint rp)1881     private static boolean equalAttributes(@NonNull TextPaint lp, @NonNull TextPaint rp) {
1882         return lp.getColorFilter() == rp.getColorFilter()
1883                 && lp.getMaskFilter() == rp.getMaskFilter()
1884                 && lp.getShader() == rp.getShader()
1885                 && lp.getTypeface() == rp.getTypeface()
1886                 && lp.getXfermode() == rp.getXfermode()
1887                 && lp.getTextLocales().equals(rp.getTextLocales())
1888                 && TextUtils.equals(lp.getFontFeatureSettings(), rp.getFontFeatureSettings())
1889                 && TextUtils.equals(lp.getFontVariationSettings(), rp.getFontVariationSettings())
1890                 && lp.getShadowLayerRadius() == rp.getShadowLayerRadius()
1891                 && lp.getShadowLayerDx() == rp.getShadowLayerDx()
1892                 && lp.getShadowLayerDy() == rp.getShadowLayerDy()
1893                 && lp.getShadowLayerColor() == rp.getShadowLayerColor()
1894                 && lp.getFlags() == rp.getFlags()
1895                 && lp.getHinting() == rp.getHinting()
1896                 && lp.getStyle() == rp.getStyle()
1897                 && lp.getColor() == rp.getColor()
1898                 && lp.getStrokeWidth() == rp.getStrokeWidth()
1899                 && lp.getStrokeMiter() == rp.getStrokeMiter()
1900                 && lp.getStrokeCap() == rp.getStrokeCap()
1901                 && lp.getStrokeJoin() == rp.getStrokeJoin()
1902                 && lp.getTextAlign() == rp.getTextAlign()
1903                 && lp.isElegantTextHeight() == rp.isElegantTextHeight()
1904                 && lp.getTextSize() == rp.getTextSize()
1905                 && lp.getTextScaleX() == rp.getTextScaleX()
1906                 && lp.getTextSkewX() == rp.getTextSkewX()
1907                 && lp.getLetterSpacing() == rp.getLetterSpacing()
1908                 && lp.getWordSpacing() == rp.getWordSpacing()
1909                 && lp.getStartHyphenEdit() == rp.getStartHyphenEdit()
1910                 && lp.getEndHyphenEdit() == rp.getEndHyphenEdit()
1911                 && lp.bgColor == rp.bgColor
1912                 && lp.baselineShift == rp.baselineShift
1913                 && lp.linkColor == rp.linkColor
1914                 && lp.drawableState == rp.drawableState
1915                 && lp.density == rp.density
1916                 && lp.underlineColor == rp.underlineColor
1917                 && lp.underlineThickness == rp.underlineThickness;
1918     }
1919 }
1920