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