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