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.FloatRange;
20 import android.annotation.IntRange;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.graphics.Paint;
24 import android.graphics.Rect;
25 import android.graphics.text.MeasuredText;
26 import android.text.AutoGrowArray.ByteArray;
27 import android.text.AutoGrowArray.FloatArray;
28 import android.text.AutoGrowArray.IntArray;
29 import android.text.Layout.Directions;
30 import android.text.style.MetricAffectingSpan;
31 import android.text.style.ReplacementSpan;
32 import android.util.Pools.SynchronizedPool;
33 
34 import java.util.Arrays;
35 
36 /**
37  * MeasuredParagraph provides text information for rendering purpose.
38  *
39  * The first motivation of this class is identify the text directions and retrieving individual
40  * character widths. However retrieving character widths is slower than identifying text directions.
41  * Thus, this class provides several builder methods for specific purposes.
42  *
43  * - buildForBidi:
44  *   Compute only text directions.
45  * - buildForMeasurement:
46  *   Compute text direction and all character widths.
47  * - buildForStaticLayout:
48  *   This is bit special. StaticLayout also needs to know text direction and character widths for
49  *   line breaking, but all things are done in native code. Similarly, text measurement is done
50  *   in native code. So instead of storing result to Java array, this keeps the result in native
51  *   code since there is no good reason to move the results to Java layer.
52  *
53  * In addition to the character widths, some additional information is computed for each purposes,
54  * e.g. whole text length for measurement or font metrics for static layout.
55  *
56  * MeasuredParagraph is NOT a thread safe object.
57  * @hide
58  */
59 public class MeasuredParagraph {
60     private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';
61 
MeasuredParagraph()62     private MeasuredParagraph() {}  // Use build static functions instead.
63 
64     private static final SynchronizedPool<MeasuredParagraph> sPool = new SynchronizedPool<>(1);
65 
obtain()66     private static @NonNull MeasuredParagraph obtain() { // Use build static functions instead.
67         final MeasuredParagraph mt = sPool.acquire();
68         return mt != null ? mt : new MeasuredParagraph();
69     }
70 
71     /**
72      * Recycle the MeasuredParagraph.
73      *
74      * Do not call any methods after you call this method.
75      */
recycle()76     public void recycle() {
77         release();
78         sPool.release(this);
79     }
80 
81     // The casted original text.
82     //
83     // This may be null if the passed text is not a Spanned.
84     private @Nullable Spanned mSpanned;
85 
86     // The start offset of the target range in the original text (mSpanned);
87     private @IntRange(from = 0) int mTextStart;
88 
89     // The length of the target range in the original text.
90     private @IntRange(from = 0) int mTextLength;
91 
92     // The copied character buffer for measuring text.
93     //
94     // The length of this array is mTextLength.
95     private @Nullable char[] mCopiedBuffer;
96 
97     // The whole paragraph direction.
98     private @Layout.Direction int mParaDir;
99 
100     // True if the text is LTR direction and doesn't contain any bidi characters.
101     private boolean mLtrWithoutBidi;
102 
103     // The bidi level for individual characters.
104     //
105     // This is empty if mLtrWithoutBidi is true.
106     private @NonNull ByteArray mLevels = new ByteArray();
107 
108     // The whole width of the text.
109     // See getWholeWidth comments.
110     private @FloatRange(from = 0.0f) float mWholeWidth;
111 
112     // Individual characters' widths.
113     // See getWidths comments.
114     private @Nullable FloatArray mWidths = new FloatArray();
115 
116     // The span end positions.
117     // See getSpanEndCache comments.
118     private @Nullable IntArray mSpanEndCache = new IntArray(4);
119 
120     // The font metrics.
121     // See getFontMetrics comments.
122     private @Nullable IntArray mFontMetrics = new IntArray(4 * 4);
123 
124     // The native MeasuredParagraph.
125     private @Nullable MeasuredText mMeasuredText;
126 
127     // Following two objects are for avoiding object allocation.
128     private @NonNull TextPaint mCachedPaint = new TextPaint();
129     private @Nullable Paint.FontMetricsInt mCachedFm;
130 
131     /**
132      * Releases internal buffers.
133      */
release()134     public void release() {
135         reset();
136         mLevels.clearWithReleasingLargeArray();
137         mWidths.clearWithReleasingLargeArray();
138         mFontMetrics.clearWithReleasingLargeArray();
139         mSpanEndCache.clearWithReleasingLargeArray();
140     }
141 
142     /**
143      * Resets the internal state for starting new text.
144      */
reset()145     private void reset() {
146         mSpanned = null;
147         mCopiedBuffer = null;
148         mWholeWidth = 0;
149         mLevels.clear();
150         mWidths.clear();
151         mFontMetrics.clear();
152         mSpanEndCache.clear();
153         mMeasuredText = null;
154     }
155 
156     /**
157      * Returns the length of the paragraph.
158      *
159      * This is always available.
160      */
getTextLength()161     public int getTextLength() {
162         return mTextLength;
163     }
164 
165     /**
166      * Returns the characters to be measured.
167      *
168      * This is always available.
169      */
getChars()170     public @NonNull char[] getChars() {
171         return mCopiedBuffer;
172     }
173 
174     /**
175      * Returns the paragraph direction.
176      *
177      * This is always available.
178      */
getParagraphDir()179     public @Layout.Direction int getParagraphDir() {
180         return mParaDir;
181     }
182 
183     /**
184      * Returns the directions.
185      *
186      * This is always available.
187      */
getDirections(@ntRangefrom = 0) int start, @IntRange(from = 0) int end)188     public Directions getDirections(@IntRange(from = 0) int start,  // inclusive
189                                     @IntRange(from = 0) int end) {  // exclusive
190         if (mLtrWithoutBidi) {
191             return Layout.DIRS_ALL_LEFT_TO_RIGHT;
192         }
193 
194         final int length = end - start;
195         return AndroidBidi.directions(mParaDir, mLevels.getRawArray(), start, mCopiedBuffer, start,
196                 length);
197     }
198 
199     /**
200      * Returns the whole text width.
201      *
202      * This is available only if the MeasuredParagraph is computed with buildForMeasurement.
203      * Returns 0 in other cases.
204      */
getWholeWidth()205     public @FloatRange(from = 0.0f) float getWholeWidth() {
206         return mWholeWidth;
207     }
208 
209     /**
210      * Returns the individual character's width.
211      *
212      * This is available only if the MeasuredParagraph is computed with buildForMeasurement.
213      * Returns empty array in other cases.
214      */
getWidths()215     public @NonNull FloatArray getWidths() {
216         return mWidths;
217     }
218 
219     /**
220      * Returns the MetricsAffectingSpan end indices.
221      *
222      * If the input text is not a spanned string, this has one value that is the length of the text.
223      *
224      * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
225      * Returns empty array in other cases.
226      */
getSpanEndCache()227     public @NonNull IntArray getSpanEndCache() {
228         return mSpanEndCache;
229     }
230 
231     /**
232      * Returns the int array which holds FontMetrics.
233      *
234      * This array holds the repeat of top, bottom, ascent, descent of font metrics value.
235      *
236      * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
237      * Returns empty array in other cases.
238      */
getFontMetrics()239     public @NonNull IntArray getFontMetrics() {
240         return mFontMetrics;
241     }
242 
243     /**
244      * Returns the native ptr of the MeasuredParagraph.
245      *
246      * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
247      * Returns null in other cases.
248      */
getMeasuredText()249     public MeasuredText getMeasuredText() {
250         return mMeasuredText;
251     }
252 
253     /**
254      * Returns the width of the given range.
255      *
256      * This is not available if the MeasuredParagraph is computed with buildForBidi.
257      * Returns 0 if the MeasuredParagraph is computed with buildForBidi.
258      *
259      * @param start the inclusive start offset of the target region in the text
260      * @param end the exclusive end offset of the target region in the text
261      */
getWidth(int start, int end)262     public float getWidth(int start, int end) {
263         if (mMeasuredText == null) {
264             // We have result in Java.
265             final float[] widths = mWidths.getRawArray();
266             float r = 0.0f;
267             for (int i = start; i < end; ++i) {
268                 r += widths[i];
269             }
270             return r;
271         } else {
272             // We have result in native.
273             return mMeasuredText.getWidth(start, end);
274         }
275     }
276 
277     /**
278      * Retrieves the bounding rectangle that encloses all of the characters, with an implied origin
279      * at (0, 0).
280      *
281      * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
282      */
getBounds(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull Rect bounds)283     public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
284             @NonNull Rect bounds) {
285         mMeasuredText.getBounds(start, end, bounds);
286     }
287 
288     /**
289      * Returns a width of the character at the offset.
290      *
291      * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
292      */
getCharWidthAt(@ntRangefrom = 0) int offset)293     public float getCharWidthAt(@IntRange(from = 0) int offset) {
294         return mMeasuredText.getCharWidthAt(offset);
295     }
296 
297     /**
298      * Generates new MeasuredParagraph for Bidi computation.
299      *
300      * If recycle is null, this returns new instance. If recycle is not null, this fills computed
301      * result to recycle and returns recycle.
302      *
303      * @param text the character sequence to be measured
304      * @param start the inclusive start offset of the target region in the text
305      * @param end the exclusive end offset of the target region in the text
306      * @param textDir the text direction
307      * @param recycle pass existing MeasuredParagraph if you want to recycle it.
308      *
309      * @return measured text
310      */
buildForBidi(@onNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir, @Nullable MeasuredParagraph recycle)311     public static @NonNull MeasuredParagraph buildForBidi(@NonNull CharSequence text,
312                                                      @IntRange(from = 0) int start,
313                                                      @IntRange(from = 0) int end,
314                                                      @NonNull TextDirectionHeuristic textDir,
315                                                      @Nullable MeasuredParagraph recycle) {
316         final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
317         mt.resetAndAnalyzeBidi(text, start, end, textDir);
318         return mt;
319     }
320 
321     /**
322      * Generates new MeasuredParagraph for measuring texts.
323      *
324      * If recycle is null, this returns new instance. If recycle is not null, this fills computed
325      * result to recycle and returns recycle.
326      *
327      * @param paint the paint to be used for rendering the text.
328      * @param text the character sequence to be measured
329      * @param start the inclusive start offset of the target region in the text
330      * @param end the exclusive end offset of the target region in the text
331      * @param textDir the text direction
332      * @param recycle pass existing MeasuredParagraph if you want to recycle it.
333      *
334      * @return measured text
335      */
buildForMeasurement(@onNull TextPaint paint, @NonNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir, @Nullable MeasuredParagraph recycle)336     public static @NonNull MeasuredParagraph buildForMeasurement(@NonNull TextPaint paint,
337                                                             @NonNull CharSequence text,
338                                                             @IntRange(from = 0) int start,
339                                                             @IntRange(from = 0) int end,
340                                                             @NonNull TextDirectionHeuristic textDir,
341                                                             @Nullable MeasuredParagraph recycle) {
342         final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
343         mt.resetAndAnalyzeBidi(text, start, end, textDir);
344 
345         mt.mWidths.resize(mt.mTextLength);
346         if (mt.mTextLength == 0) {
347             return mt;
348         }
349 
350         if (mt.mSpanned == null) {
351             // No style change by MetricsAffectingSpan. Just measure all text.
352             mt.applyMetricsAffectingSpan(
353                     paint, null /* spans */, start, end, null /* native builder ptr */);
354         } else {
355             // There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
356             int spanEnd;
357             for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
358                 spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class);
359                 MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
360                         MetricAffectingSpan.class);
361                 spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
362                 mt.applyMetricsAffectingSpan(
363                         paint, spans, spanStart, spanEnd, null /* native builder ptr */);
364             }
365         }
366         return mt;
367     }
368 
369     /**
370      * Generates new MeasuredParagraph for StaticLayout.
371      *
372      * If recycle is null, this returns new instance. If recycle is not null, this fills computed
373      * result to recycle and returns recycle.
374      *
375      * @param paint the paint to be used for rendering the text.
376      * @param text the character sequence to be measured
377      * @param start the inclusive start offset of the target region in the text
378      * @param end the exclusive end offset of the target region in the text
379      * @param textDir the text direction
380      * @param computeHyphenation true if need to compute hyphenation, otherwise false
381      * @param computeLayout true if need to compute full layout, otherwise false.
382      * @param hint pass if you already have measured paragraph.
383      * @param recycle pass existing MeasuredParagraph if you want to recycle it.
384      *
385      * @return measured text
386      */
buildForStaticLayout( @onNull TextPaint paint, @NonNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir, boolean computeHyphenation, boolean computeLayout, @Nullable MeasuredParagraph hint, @Nullable MeasuredParagraph recycle)387     public static @NonNull MeasuredParagraph buildForStaticLayout(
388             @NonNull TextPaint paint,
389             @NonNull CharSequence text,
390             @IntRange(from = 0) int start,
391             @IntRange(from = 0) int end,
392             @NonNull TextDirectionHeuristic textDir,
393             boolean computeHyphenation,
394             boolean computeLayout,
395             @Nullable MeasuredParagraph hint,
396             @Nullable MeasuredParagraph recycle) {
397         final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
398         mt.resetAndAnalyzeBidi(text, start, end, textDir);
399         final MeasuredText.Builder builder;
400         if (hint == null) {
401             builder = new MeasuredText.Builder(mt.mCopiedBuffer)
402                     .setComputeHyphenation(computeHyphenation)
403                     .setComputeLayout(computeLayout);
404         } else {
405             builder = new MeasuredText.Builder(hint.mMeasuredText);
406         }
407         if (mt.mTextLength == 0) {
408             // Need to build empty native measured text for StaticLayout.
409             // TODO: Stop creating empty measured text for empty lines.
410             mt.mMeasuredText = builder.build();
411         } else {
412             if (mt.mSpanned == null) {
413                 // No style change by MetricsAffectingSpan. Just measure all text.
414                 mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end, builder);
415                 mt.mSpanEndCache.append(end);
416             } else {
417                 // There may be a MetricsAffectingSpan. Split into span transitions and apply
418                 // styles.
419                 int spanEnd;
420                 for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
421                     spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end,
422                                                              MetricAffectingSpan.class);
423                     MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
424                             MetricAffectingSpan.class);
425                     spans = TextUtils.removeEmptySpans(spans, mt.mSpanned,
426                                                        MetricAffectingSpan.class);
427                     mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd, builder);
428                     mt.mSpanEndCache.append(spanEnd);
429                 }
430             }
431             mt.mMeasuredText = builder.build();
432         }
433 
434         return mt;
435     }
436 
437     /**
438      * Reset internal state and analyzes text for bidirectional runs.
439      *
440      * @param text the character sequence to be measured
441      * @param start the inclusive start offset of the target region in the text
442      * @param end the exclusive end offset of the target region in the text
443      * @param textDir the text direction
444      */
resetAndAnalyzeBidi(@onNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir)445     private void resetAndAnalyzeBidi(@NonNull CharSequence text,
446                                      @IntRange(from = 0) int start,  // inclusive
447                                      @IntRange(from = 0) int end,  // exclusive
448                                      @NonNull TextDirectionHeuristic textDir) {
449         reset();
450         mSpanned = text instanceof Spanned ? (Spanned) text : null;
451         mTextStart = start;
452         mTextLength = end - start;
453 
454         if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) {
455             mCopiedBuffer = new char[mTextLength];
456         }
457         TextUtils.getChars(text, start, end, mCopiedBuffer, 0);
458 
459         // Replace characters associated with ReplacementSpan to U+FFFC.
460         if (mSpanned != null) {
461             ReplacementSpan[] spans = mSpanned.getSpans(start, end, ReplacementSpan.class);
462 
463             for (int i = 0; i < spans.length; i++) {
464                 int startInPara = mSpanned.getSpanStart(spans[i]) - start;
465                 int endInPara = mSpanned.getSpanEnd(spans[i]) - start;
466                 // The span interval may be larger and must be restricted to [start, end)
467                 if (startInPara < 0) startInPara = 0;
468                 if (endInPara > mTextLength) endInPara = mTextLength;
469                 Arrays.fill(mCopiedBuffer, startInPara, endInPara, OBJECT_REPLACEMENT_CHARACTER);
470             }
471         }
472 
473         if ((textDir == TextDirectionHeuristics.LTR
474                 || textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR
475                 || textDir == TextDirectionHeuristics.ANYRTL_LTR)
476                 && TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) {
477             mLevels.clear();
478             mParaDir = Layout.DIR_LEFT_TO_RIGHT;
479             mLtrWithoutBidi = true;
480         } else {
481             final int bidiRequest;
482             if (textDir == TextDirectionHeuristics.LTR) {
483                 bidiRequest = Layout.DIR_REQUEST_LTR;
484             } else if (textDir == TextDirectionHeuristics.RTL) {
485                 bidiRequest = Layout.DIR_REQUEST_RTL;
486             } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) {
487                 bidiRequest = Layout.DIR_REQUEST_DEFAULT_LTR;
488             } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
489                 bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL;
490             } else {
491                 final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength);
492                 bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR;
493             }
494             mLevels.resize(mTextLength);
495             mParaDir = AndroidBidi.bidi(bidiRequest, mCopiedBuffer, mLevels.getRawArray());
496             mLtrWithoutBidi = false;
497         }
498     }
499 
applyReplacementRun(@onNull ReplacementSpan replacement, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @Nullable MeasuredText.Builder builder)500     private void applyReplacementRun(@NonNull ReplacementSpan replacement,
501                                      @IntRange(from = 0) int start,  // inclusive, in copied buffer
502                                      @IntRange(from = 0) int end,  // exclusive, in copied buffer
503                                      @Nullable MeasuredText.Builder builder) {
504         // Use original text. Shouldn't matter.
505         // TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for
506         //       backward compatibility? or Should we initialize them for getFontMetricsInt?
507         final float width = replacement.getSize(
508                 mCachedPaint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm);
509         if (builder == null) {
510             // Assigns all width to the first character. This is the same behavior as minikin.
511             mWidths.set(start, width);
512             if (end > start + 1) {
513                 Arrays.fill(mWidths.getRawArray(), start + 1, end, 0.0f);
514             }
515             mWholeWidth += width;
516         } else {
517             builder.appendReplacementRun(mCachedPaint, end - start, width);
518         }
519     }
520 
applyStyleRun(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @Nullable MeasuredText.Builder builder)521     private void applyStyleRun(@IntRange(from = 0) int start,  // inclusive, in copied buffer
522                                @IntRange(from = 0) int end,  // exclusive, in copied buffer
523                                @Nullable MeasuredText.Builder builder) {
524 
525         if (mLtrWithoutBidi) {
526             // If the whole text is LTR direction, just apply whole region.
527             if (builder == null) {
528                 mWholeWidth += mCachedPaint.getTextRunAdvances(
529                         mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */,
530                         mWidths.getRawArray(), start);
531             } else {
532                 builder.appendStyleRun(mCachedPaint, end - start, false /* isRtl */);
533             }
534         } else {
535             // If there is multiple bidi levels, split into individual bidi level and apply style.
536             byte level = mLevels.get(start);
537             // Note that the empty text or empty range won't reach this method.
538             // Safe to search from start + 1.
539             for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) {
540                 if (levelEnd == end || mLevels.get(levelEnd) != level) {  // transition point
541                     final boolean isRtl = (level & 0x1) != 0;
542                     if (builder == null) {
543                         final int levelLength = levelEnd - levelStart;
544                         mWholeWidth += mCachedPaint.getTextRunAdvances(
545                                 mCopiedBuffer, levelStart, levelLength, levelStart, levelLength,
546                                 isRtl, mWidths.getRawArray(), levelStart);
547                     } else {
548                         builder.appendStyleRun(mCachedPaint, levelEnd - levelStart, isRtl);
549                     }
550                     if (levelEnd == end) {
551                         break;
552                     }
553                     levelStart = levelEnd;
554                     level = mLevels.get(levelEnd);
555                 }
556             }
557         }
558     }
559 
applyMetricsAffectingSpan( @onNull TextPaint paint, @Nullable MetricAffectingSpan[] spans, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @Nullable MeasuredText.Builder builder)560     private void applyMetricsAffectingSpan(
561             @NonNull TextPaint paint,
562             @Nullable MetricAffectingSpan[] spans,
563             @IntRange(from = 0) int start,  // inclusive, in original text buffer
564             @IntRange(from = 0) int end,  // exclusive, in original text buffer
565             @Nullable MeasuredText.Builder builder) {
566         mCachedPaint.set(paint);
567         // XXX paint should not have a baseline shift, but...
568         mCachedPaint.baselineShift = 0;
569 
570         final boolean needFontMetrics = builder != null;
571 
572         if (needFontMetrics && mCachedFm == null) {
573             mCachedFm = new Paint.FontMetricsInt();
574         }
575 
576         ReplacementSpan replacement = null;
577         if (spans != null) {
578             for (int i = 0; i < spans.length; i++) {
579                 MetricAffectingSpan span = spans[i];
580                 if (span instanceof ReplacementSpan) {
581                     // The last ReplacementSpan is effective for backward compatibility reasons.
582                     replacement = (ReplacementSpan) span;
583                 } else {
584                     // TODO: No need to call updateMeasureState for ReplacementSpan as well?
585                     span.updateMeasureState(mCachedPaint);
586                 }
587             }
588         }
589 
590         final int startInCopiedBuffer = start - mTextStart;
591         final int endInCopiedBuffer = end - mTextStart;
592 
593         if (builder != null) {
594             mCachedPaint.getFontMetricsInt(mCachedFm);
595         }
596 
597         if (replacement != null) {
598             applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer, builder);
599         } else {
600             applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, builder);
601         }
602 
603         if (needFontMetrics) {
604             if (mCachedPaint.baselineShift < 0) {
605                 mCachedFm.ascent += mCachedPaint.baselineShift;
606                 mCachedFm.top += mCachedPaint.baselineShift;
607             } else {
608                 mCachedFm.descent += mCachedPaint.baselineShift;
609                 mCachedFm.bottom += mCachedPaint.baselineShift;
610             }
611 
612             mFontMetrics.append(mCachedFm.top);
613             mFontMetrics.append(mCachedFm.bottom);
614             mFontMetrics.append(mCachedFm.ascent);
615             mFontMetrics.append(mCachedFm.descent);
616         }
617     }
618 
619     /**
620      * Returns the maximum index that the accumulated width not exceeds the width.
621      *
622      * If forward=false is passed, returns the minimum index from the end instead.
623      *
624      * This only works if the MeasuredParagraph is computed with buildForMeasurement.
625      * Undefined behavior in other case.
626      */
breakText(int limit, boolean forwards, float width)627     @IntRange(from = 0) int breakText(int limit, boolean forwards, float width) {
628         float[] w = mWidths.getRawArray();
629         if (forwards) {
630             int i = 0;
631             while (i < limit) {
632                 width -= w[i];
633                 if (width < 0.0f) break;
634                 i++;
635             }
636             while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--;
637             return i;
638         } else {
639             int i = limit - 1;
640             while (i >= 0) {
641                 width -= w[i];
642                 if (width < 0.0f) break;
643                 i--;
644             }
645             while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) {
646                 i++;
647             }
648             return limit - i - 1;
649         }
650     }
651 
652     /**
653      * Returns the length of the substring.
654      *
655      * This only works if the MeasuredParagraph is computed with buildForMeasurement.
656      * Undefined behavior in other case.
657      */
measure(int start, int limit)658     @FloatRange(from = 0.0f) float measure(int start, int limit) {
659         float width = 0;
660         float[] w = mWidths.getRawArray();
661         for (int i = start; i < limit; ++i) {
662             width += w[i];
663         }
664         return width;
665     }
666 
667     /**
668      * This only works if the MeasuredParagraph is computed with buildForStaticLayout.
669      */
getMemoryUsage()670     public @IntRange(from = 0) int getMemoryUsage() {
671         return mMeasuredText.getMemoryUsage();
672     }
673 }
674