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