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 static com.android.text.flags.Flags.FLAG_NO_BREAK_NO_HYPHENATION_SPAN;
20 
21 import android.annotation.FlaggedApi;
22 import android.annotation.FloatRange;
23 import android.annotation.IntRange;
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.annotation.Px;
27 import android.annotation.SuppressLint;
28 import android.annotation.TestApi;
29 import android.graphics.Paint;
30 import android.graphics.Rect;
31 import android.graphics.text.LineBreakConfig;
32 import android.graphics.text.MeasuredText;
33 import android.icu.lang.UCharacter;
34 import android.icu.lang.UCharacterDirection;
35 import android.icu.text.Bidi;
36 import android.text.AutoGrowArray.ByteArray;
37 import android.text.AutoGrowArray.FloatArray;
38 import android.text.AutoGrowArray.IntArray;
39 import android.text.Layout.Directions;
40 import android.text.style.LineBreakConfigSpan;
41 import android.text.style.MetricAffectingSpan;
42 import android.text.style.ReplacementSpan;
43 import android.util.Pools.SynchronizedPool;
44 
45 import java.util.Arrays;
46 
47 /**
48  * MeasuredParagraph provides text information for rendering purpose.
49  *
50  * The first motivation of this class is identify the text directions and retrieving individual
51  * character widths. However retrieving character widths is slower than identifying text directions.
52  * Thus, this class provides several builder methods for specific purposes.
53  *
54  * - buildForBidi:
55  *   Compute only text directions.
56  * - buildForMeasurement:
57  *   Compute text direction and all character widths.
58  * - buildForStaticLayout:
59  *   This is bit special. StaticLayout also needs to know text direction and character widths for
60  *   line breaking, but all things are done in native code. Similarly, text measurement is done
61  *   in native code. So instead of storing result to Java array, this keeps the result in native
62  *   code since there is no good reason to move the results to Java layer.
63  *
64  * In addition to the character widths, some additional information is computed for each purposes,
65  * e.g. whole text length for measurement or font metrics for static layout.
66  *
67  * MeasuredParagraph is NOT a thread safe object.
68  * @hide
69  */
70 @TestApi
71 public class MeasuredParagraph {
72     private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';
73 
MeasuredParagraph()74     private MeasuredParagraph() {}  // Use build static functions instead.
75 
76     private static final SynchronizedPool<MeasuredParagraph> sPool = new SynchronizedPool<>(1);
77 
obtain()78     private static @NonNull MeasuredParagraph obtain() { // Use build static functions instead.
79         final MeasuredParagraph mt = sPool.acquire();
80         return mt != null ? mt : new MeasuredParagraph();
81     }
82 
83     /**
84      * Recycle the MeasuredParagraph.
85      *
86      * Do not call any methods after you call this method.
87      * @hide
88      */
recycle()89     public void recycle() {
90         release();
91         sPool.release(this);
92     }
93 
94     // The casted original text.
95     //
96     // This may be null if the passed text is not a Spanned.
97     private @Nullable Spanned mSpanned;
98 
99     // The start offset of the target range in the original text (mSpanned);
100     private @IntRange(from = 0) int mTextStart;
101 
102     // The length of the target range in the original text.
103     private @IntRange(from = 0) int mTextLength;
104 
105     // The copied character buffer for measuring text.
106     //
107     // The length of this array is mTextLength.
108     private @Nullable char[] mCopiedBuffer;
109 
110     // The whole paragraph direction.
111     private @Layout.Direction int mParaDir;
112 
113     // True if the text is LTR direction and doesn't contain any bidi characters.
114     private boolean mLtrWithoutBidi;
115 
116     // The bidi level for individual characters.
117     //
118     // This is empty if mLtrWithoutBidi is true.
119     private @NonNull ByteArray mLevels = new ByteArray();
120 
121     private Bidi mBidi;
122 
123     // The whole width of the text.
124     // See getWholeWidth comments.
125     private @FloatRange(from = 0.0f) float mWholeWidth;
126 
127     // Individual characters' widths.
128     // See getWidths comments.
129     private @Nullable FloatArray mWidths = new FloatArray();
130 
131     // The span end positions.
132     // See getSpanEndCache comments.
133     private @Nullable IntArray mSpanEndCache = new IntArray(4);
134 
135     // The font metrics.
136     // See getFontMetrics comments.
137     private @Nullable IntArray mFontMetrics = new IntArray(4 * 4);
138 
139     // The native MeasuredParagraph.
140     private @Nullable MeasuredText mMeasuredText;
141 
142     // Following three objects are for avoiding object allocation.
143     private final @NonNull TextPaint mCachedPaint = new TextPaint();
144     private @Nullable Paint.FontMetricsInt mCachedFm;
145     private final @NonNull LineBreakConfig.Builder mLineBreakConfigBuilder =
146             new LineBreakConfig.Builder();
147 
148     /**
149      * Releases internal buffers.
150      * @hide
151      */
release()152     public void release() {
153         reset();
154         mLevels.clearWithReleasingLargeArray();
155         mWidths.clearWithReleasingLargeArray();
156         mFontMetrics.clearWithReleasingLargeArray();
157         mSpanEndCache.clearWithReleasingLargeArray();
158     }
159 
160     /**
161      * Resets the internal state for starting new text.
162      */
reset()163     private void reset() {
164         mSpanned = null;
165         mCopiedBuffer = null;
166         mWholeWidth = 0;
167         mLevels.clear();
168         mWidths.clear();
169         mFontMetrics.clear();
170         mSpanEndCache.clear();
171         mMeasuredText = null;
172         mBidi = null;
173     }
174 
175     /**
176      * Returns the length of the paragraph.
177      *
178      * This is always available.
179      * @hide
180      */
getTextLength()181     public int getTextLength() {
182         return mTextLength;
183     }
184 
185     /**
186      * Returns the characters to be measured.
187      *
188      * This is always available.
189      * @hide
190      */
getChars()191     public @NonNull char[] getChars() {
192         return mCopiedBuffer;
193     }
194 
195     /**
196      * Returns the paragraph direction.
197      *
198      * This is always available.
199      * @hide
200      */
getParagraphDir()201     public @Layout.Direction int getParagraphDir() {
202         if (ClientFlags.icuBidiMigration()) {
203             if (mBidi == null) {
204                 return Layout.DIR_LEFT_TO_RIGHT;
205             }
206             return (mBidi.getParaLevel() & 0x01) == 0
207                     ? Layout.DIR_LEFT_TO_RIGHT : Layout.DIR_RIGHT_TO_LEFT;
208         }
209         return mParaDir;
210     }
211 
212     /**
213      * Returns the directions.
214      *
215      * This is always available.
216      * @hide
217      */
getDirections(@ntRangefrom = 0) int start, @IntRange(from = 0) int end)218     public Directions getDirections(@IntRange(from = 0) int start,  // inclusive
219                                     @IntRange(from = 0) int end) {  // exclusive
220         if (ClientFlags.icuBidiMigration()) {
221             // Easy case: mBidi == null means the text is all LTR and no bidi suppot is needed.
222             if (mBidi == null) {
223                 return Layout.DIRS_ALL_LEFT_TO_RIGHT;
224             }
225 
226             // Easy case: If the original text only contains single directionality run, the
227             // substring is only single run.
228             if (start == end) {
229                 if ((mBidi.getParaLevel() & 0x01) == 0) {
230                     return Layout.DIRS_ALL_LEFT_TO_RIGHT;
231                 } else {
232                     return Layout.DIRS_ALL_RIGHT_TO_LEFT;
233                 }
234             }
235 
236             // Okay, now we need to generate the line instance.
237             Bidi bidi = mBidi.createLineBidi(start, end);
238 
239             // Easy case: If the line instance only contains single directionality run, no need
240             // to reorder visually.
241             if (bidi.getRunCount() == 1) {
242                 if (bidi.getRunLevel(0) == 1) {
243                     return Layout.DIRS_ALL_RIGHT_TO_LEFT;
244                 } else if (bidi.getRunLevel(0) == 0) {
245                     return Layout.DIRS_ALL_LEFT_TO_RIGHT;
246                 } else {
247                     return new Directions(new int[] {
248                             0, bidi.getRunLevel(0) << Layout.RUN_LEVEL_SHIFT | (end - start)});
249                 }
250             }
251 
252             // Reorder directionality run visually.
253             byte[] levels = new byte[bidi.getRunCount()];
254             for (int i = 0; i < bidi.getRunCount(); ++i) {
255                 levels[i] = (byte) bidi.getRunLevel(i);
256             }
257             int[] visualOrders = Bidi.reorderVisual(levels);
258 
259             int[] dirs = new int[bidi.getRunCount() * 2];
260             for (int i = 0; i < bidi.getRunCount(); ++i) {
261                 int vIndex;
262                 if ((mBidi.getBaseLevel() & 0x01) == 1) {
263                     // For the historical reasons, if the base directionality is RTL, the Android
264                     // draws from the right, i.e. the visually reordered run needs to be reversed.
265                     vIndex = visualOrders[bidi.getRunCount() - i - 1];
266                 } else {
267                     vIndex = visualOrders[i];
268                 }
269 
270                 // Special packing of dire
271                 dirs[i * 2] = bidi.getRunStart(vIndex);
272                 dirs[i * 2 + 1] = bidi.getRunLevel(vIndex) << Layout.RUN_LEVEL_SHIFT
273                         | (bidi.getRunLimit(vIndex) - dirs[i * 2]);
274             }
275 
276             return new Directions(dirs);
277         }
278         if (mLtrWithoutBidi) {
279             return Layout.DIRS_ALL_LEFT_TO_RIGHT;
280         }
281 
282         final int length = end - start;
283         return AndroidBidi.directions(mParaDir, mLevels.getRawArray(), start, mCopiedBuffer, start,
284                 length);
285     }
286 
287     /**
288      * Returns the whole text width.
289      *
290      * This is available only if the MeasuredParagraph is computed with buildForMeasurement.
291      * Returns 0 in other cases.
292      * @hide
293      */
getWholeWidth()294     public @FloatRange(from = 0.0f) float getWholeWidth() {
295         return mWholeWidth;
296     }
297 
298     /**
299      * Returns the individual character's width.
300      *
301      * This is available only if the MeasuredParagraph is computed with buildForMeasurement.
302      * Returns empty array in other cases.
303      * @hide
304      */
getWidths()305     public @NonNull FloatArray getWidths() {
306         return mWidths;
307     }
308 
309     /**
310      * Returns the MetricsAffectingSpan end indices.
311      *
312      * If the input text is not a spanned string, this has one value that is the length of the text.
313      *
314      * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
315      * Returns empty array in other cases.
316      * @hide
317      */
getSpanEndCache()318     public @NonNull IntArray getSpanEndCache() {
319         return mSpanEndCache;
320     }
321 
322     /**
323      * Returns the int array which holds FontMetrics.
324      *
325      * This array holds the repeat of top, bottom, ascent, descent of font metrics value.
326      *
327      * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
328      * Returns empty array in other cases.
329      * @hide
330      */
getFontMetrics()331     public @NonNull IntArray getFontMetrics() {
332         return mFontMetrics;
333     }
334 
335     /**
336      * Returns the native ptr of the MeasuredParagraph.
337      *
338      * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
339      * Returns null in other cases.
340      * @hide
341      */
getMeasuredText()342     public MeasuredText getMeasuredText() {
343         return mMeasuredText;
344     }
345 
346     /**
347      * Returns the width of the given range.
348      *
349      * This is not available if the MeasuredParagraph is computed with buildForBidi.
350      * Returns 0 if the MeasuredParagraph is computed with buildForBidi.
351      *
352      * @param start the inclusive start offset of the target region in the text
353      * @param end the exclusive end offset of the target region in the text
354      * @hide
355      */
getWidth(int start, int end)356     public float getWidth(int start, int end) {
357         if (mMeasuredText == null) {
358             // We have result in Java.
359             final float[] widths = mWidths.getRawArray();
360             float r = 0.0f;
361             for (int i = start; i < end; ++i) {
362                 r += widths[i];
363             }
364             return r;
365         } else {
366             // We have result in native.
367             return mMeasuredText.getWidth(start, end);
368         }
369     }
370 
371     /**
372      * Retrieves the bounding rectangle that encloses all of the characters, with an implied origin
373      * at (0, 0).
374      *
375      * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
376      * @hide
377      */
getBounds(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull Rect bounds)378     public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
379             @NonNull Rect bounds) {
380         mMeasuredText.getBounds(start, end, bounds);
381     }
382 
383     /**
384      * Retrieves the font metrics for the given range.
385      *
386      * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
387      * @hide
388      */
getFontMetricsInt(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull Paint.FontMetricsInt fmi)389     public void getFontMetricsInt(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
390             @NonNull Paint.FontMetricsInt fmi) {
391         mMeasuredText.getFontMetricsInt(start, end, fmi);
392     }
393 
394     /**
395      * Returns a width of the character at the offset.
396      *
397      * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
398      * @hide
399      */
getCharWidthAt(@ntRangefrom = 0) int offset)400     public float getCharWidthAt(@IntRange(from = 0) int offset) {
401         return mMeasuredText.getCharWidthAt(offset);
402     }
403 
404     /**
405      * Generates new MeasuredParagraph for Bidi computation.
406      *
407      * If recycle is null, this returns new instance. If recycle is not null, this fills computed
408      * result to recycle and returns recycle.
409      *
410      * @param text the character sequence to be measured
411      * @param start the inclusive start offset of the target region in the text
412      * @param end the exclusive end offset of the target region in the text
413      * @param textDir the text direction
414      * @param recycle pass existing MeasuredParagraph if you want to recycle it.
415      *
416      * @return measured text
417      * @hide
418      */
buildForBidi(@onNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir, @Nullable MeasuredParagraph recycle)419     public static @NonNull MeasuredParagraph buildForBidi(@NonNull CharSequence text,
420                                                      @IntRange(from = 0) int start,
421                                                      @IntRange(from = 0) int end,
422                                                      @NonNull TextDirectionHeuristic textDir,
423                                                      @Nullable MeasuredParagraph recycle) {
424         final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
425         mt.resetAndAnalyzeBidi(text, start, end, textDir);
426         return mt;
427     }
428 
429     /**
430      * Generates new MeasuredParagraph for measuring texts.
431      *
432      * If recycle is null, this returns new instance. If recycle is not null, this fills computed
433      * result to recycle and returns recycle.
434      *
435      * @param paint the paint to be used for rendering the text.
436      * @param text the character sequence to be measured
437      * @param start the inclusive start offset of the target region in the text
438      * @param end the exclusive end offset of the target region in the text
439      * @param textDir the text direction
440      * @param recycle pass existing MeasuredParagraph if you want to recycle it.
441      *
442      * @return measured text
443      * @hide
444      */
buildForMeasurement(@onNull TextPaint paint, @NonNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir, @Nullable MeasuredParagraph recycle)445     public static @NonNull MeasuredParagraph buildForMeasurement(@NonNull TextPaint paint,
446                                                             @NonNull CharSequence text,
447                                                             @IntRange(from = 0) int start,
448                                                             @IntRange(from = 0) int end,
449                                                             @NonNull TextDirectionHeuristic textDir,
450                                                             @Nullable MeasuredParagraph recycle) {
451         final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
452         mt.resetAndAnalyzeBidi(text, start, end, textDir);
453 
454         mt.mWidths.resize(mt.mTextLength);
455         if (mt.mTextLength == 0) {
456             return mt;
457         }
458 
459         if (mt.mSpanned == null) {
460             // No style change by MetricsAffectingSpan. Just measure all text.
461             mt.applyMetricsAffectingSpan(
462                     paint, null /* lineBreakConfig */, null /* spans */, null /* lbcSpans */,
463                     start, end, null /* native builder ptr */, null);
464         } else {
465             // There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
466             int spanEnd;
467             for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
468                 int maSpanEnd = mt.mSpanned.nextSpanTransition(spanStart, end,
469                         MetricAffectingSpan.class);
470                 int lbcSpanEnd = mt.mSpanned.nextSpanTransition(spanStart, end,
471                         LineBreakConfigSpan.class);
472                 spanEnd = Math.min(maSpanEnd, lbcSpanEnd);
473                 MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
474                         MetricAffectingSpan.class);
475                 LineBreakConfigSpan[] lbcSpans = mt.mSpanned.getSpans(spanStart, spanEnd,
476                         LineBreakConfigSpan.class);
477                 spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
478                 lbcSpans = TextUtils.removeEmptySpans(lbcSpans, mt.mSpanned,
479                         LineBreakConfigSpan.class);
480                 mt.applyMetricsAffectingSpan(
481                         paint, null /* line break config */, spans, lbcSpans, spanStart, spanEnd,
482                         null /* native builder ptr */, null);
483             }
484         }
485         return mt;
486     }
487 
488     /**
489      * A test interface for observing the style run calculation.
490      * @hide
491      */
492     @TestApi
493     @FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN)
494     public interface StyleRunCallback {
495         /**
496          * Called when a single style run is identified.
497          */
498         @FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN)
onAppendStyleRun(@onNull Paint paint, @Nullable LineBreakConfig lineBreakConfig, @IntRange(from = 0) int length, boolean isRtl)499         void onAppendStyleRun(@NonNull Paint paint,
500                 @Nullable LineBreakConfig lineBreakConfig, @IntRange(from = 0) int length,
501                 boolean isRtl);
502 
503         /**
504          * Called when a single replacement run is identified.
505          */
506         @FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN)
onAppendReplacementRun(@onNull Paint paint, @IntRange(from = 0) int length, @Px @FloatRange(from = 0) float width)507         void onAppendReplacementRun(@NonNull Paint paint,
508                 @IntRange(from = 0) int length, @Px @FloatRange(from = 0) float width);
509     }
510 
511     /**
512      * Generates new MeasuredParagraph for StaticLayout.
513      *
514      * If recycle is null, this returns new instance. If recycle is not null, this fills computed
515      * result to recycle and returns recycle.
516      *
517      * @param paint the paint to be used for rendering the text.
518      * @param lineBreakConfig the line break configuration for text wrapping.
519      * @param text the character sequence to be measured
520      * @param start the inclusive start offset of the target region in the text
521      * @param end the exclusive end offset of the target region in the text
522      * @param textDir the text direction
523      * @param hyphenationMode a hyphenation mode
524      * @param computeLayout true if need to compute full layout, otherwise false.
525      * @param hint pass if you already have measured paragraph.
526      * @param recycle pass existing MeasuredParagraph if you want to recycle it.
527      *
528      * @return measured text
529      * @hide
530      */
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, boolean computeBounds, @Nullable MeasuredParagraph hint, @Nullable MeasuredParagraph recycle)531     public static @NonNull MeasuredParagraph buildForStaticLayout(
532             @NonNull TextPaint paint,
533             @Nullable LineBreakConfig lineBreakConfig,
534             @NonNull CharSequence text,
535             @IntRange(from = 0) int start,
536             @IntRange(from = 0) int end,
537             @NonNull TextDirectionHeuristic textDir,
538             int hyphenationMode,
539             boolean computeLayout,
540             boolean computeBounds,
541             @Nullable MeasuredParagraph hint,
542             @Nullable MeasuredParagraph recycle) {
543         return buildForStaticLayoutInternal(paint, lineBreakConfig, text, start, end, textDir,
544                 hyphenationMode, computeLayout, computeBounds, hint, recycle, null);
545     }
546 
547     /**
548      * Generates new MeasuredParagraph for StaticLayout.
549      *
550      * If recycle is null, this returns new instance. If recycle is not null, this fills computed
551      * result to recycle and returns recycle.
552      *
553      * @param paint the paint to be used for rendering the text.
554      * @param lineBreakConfig the line break configuration for text wrapping.
555      * @param text the character sequence to be measured
556      * @param start the inclusive start offset of the target region in the text
557      * @param end the exclusive end offset of the target region in the text
558      * @param textDir the text direction
559      * @param hyphenationMode a hyphenation mode
560      * @param computeLayout true if need to compute full layout, otherwise false.
561      *
562      * @return measured text
563      * @hide
564      */
565     @SuppressLint("ExecutorRegistration")
566     @TestApi
567     @NonNull
568     @FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN)
buildForStaticLayoutTest( @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 StyleRunCallback testCallback)569     public static MeasuredParagraph buildForStaticLayoutTest(
570             @NonNull TextPaint paint,
571             @Nullable LineBreakConfig lineBreakConfig,
572             @NonNull CharSequence text,
573             @IntRange(from = 0) int start,
574             @IntRange(from = 0) int end,
575             @NonNull TextDirectionHeuristic textDir,
576             int hyphenationMode,
577             boolean computeLayout,
578             @Nullable StyleRunCallback testCallback) {
579         return buildForStaticLayoutInternal(paint, lineBreakConfig, text, start, end, textDir,
580                 hyphenationMode, computeLayout, false, null, null, testCallback);
581     }
582 
buildForStaticLayoutInternal( @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, boolean computeBounds, @Nullable MeasuredParagraph hint, @Nullable MeasuredParagraph recycle, @Nullable StyleRunCallback testCallback)583     private static @NonNull MeasuredParagraph buildForStaticLayoutInternal(
584             @NonNull TextPaint paint,
585             @Nullable LineBreakConfig lineBreakConfig,
586             @NonNull CharSequence text,
587             @IntRange(from = 0) int start,
588             @IntRange(from = 0) int end,
589             @NonNull TextDirectionHeuristic textDir,
590             int hyphenationMode,
591             boolean computeLayout,
592             boolean computeBounds,
593             @Nullable MeasuredParagraph hint,
594             @Nullable MeasuredParagraph recycle,
595             @Nullable StyleRunCallback testCallback) {
596         final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
597         mt.resetAndAnalyzeBidi(text, start, end, textDir);
598         final MeasuredText.Builder builder;
599         if (hint == null) {
600             builder = new MeasuredText.Builder(mt.mCopiedBuffer)
601                     .setComputeHyphenation(hyphenationMode)
602                     .setComputeLayout(computeLayout)
603                     .setComputeBounds(computeBounds);
604         } else {
605             builder = new MeasuredText.Builder(hint.mMeasuredText);
606         }
607         if (mt.mTextLength == 0) {
608             // Need to build empty native measured text for StaticLayout.
609             // TODO: Stop creating empty measured text for empty lines.
610             mt.mMeasuredText = builder.build();
611         } else {
612             if (mt.mSpanned == null) {
613                 // No style change by MetricsAffectingSpan. Just measure all text.
614                 mt.applyMetricsAffectingSpan(paint, lineBreakConfig, null /* spans */, null,
615                         start, end, builder, testCallback);
616                 mt.mSpanEndCache.append(end);
617             } else {
618                 // There may be a MetricsAffectingSpan. Split into span transitions and apply
619                 // styles.
620                 int spanEnd;
621                 for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
622                     int maSpanEnd = mt.mSpanned.nextSpanTransition(spanStart, end,
623                                                              MetricAffectingSpan.class);
624                     int lbcSpanEnd = mt.mSpanned.nextSpanTransition(spanStart, end,
625                             LineBreakConfigSpan.class);
626                     spanEnd = Math.min(maSpanEnd, lbcSpanEnd);
627                     MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
628                             MetricAffectingSpan.class);
629                     LineBreakConfigSpan[] lbcSpans = mt.mSpanned.getSpans(spanStart, spanEnd,
630                             LineBreakConfigSpan.class);
631                     spans = TextUtils.removeEmptySpans(spans, mt.mSpanned,
632                                                        MetricAffectingSpan.class);
633                     lbcSpans = TextUtils.removeEmptySpans(lbcSpans, mt.mSpanned,
634                                                        LineBreakConfigSpan.class);
635                     mt.applyMetricsAffectingSpan(paint, lineBreakConfig, spans, lbcSpans, spanStart,
636                             spanEnd, builder, testCallback);
637                     mt.mSpanEndCache.append(spanEnd);
638                 }
639             }
640             mt.mMeasuredText = builder.build();
641         }
642 
643         return mt;
644     }
645 
646     /**
647      * Reset internal state and analyzes text for bidirectional runs.
648      *
649      * @param text the character sequence to be measured
650      * @param start the inclusive start offset of the target region in the text
651      * @param end the exclusive end offset of the target region in the text
652      * @param textDir the text direction
653      */
resetAndAnalyzeBidi(@onNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir)654     private void resetAndAnalyzeBidi(@NonNull CharSequence text,
655                                      @IntRange(from = 0) int start,  // inclusive
656                                      @IntRange(from = 0) int end,  // exclusive
657                                      @NonNull TextDirectionHeuristic textDir) {
658         reset();
659         mSpanned = text instanceof Spanned ? (Spanned) text : null;
660         mTextStart = start;
661         mTextLength = end - start;
662 
663         if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) {
664             mCopiedBuffer = new char[mTextLength];
665         }
666         TextUtils.getChars(text, start, end, mCopiedBuffer, 0);
667 
668         // Replace characters associated with ReplacementSpan to U+FFFC.
669         if (mSpanned != null) {
670             ReplacementSpan[] spans = mSpanned.getSpans(start, end, ReplacementSpan.class);
671 
672             for (int i = 0; i < spans.length; i++) {
673                 int startInPara = mSpanned.getSpanStart(spans[i]) - start;
674                 int endInPara = mSpanned.getSpanEnd(spans[i]) - start;
675                 // The span interval may be larger and must be restricted to [start, end)
676                 if (startInPara < 0) startInPara = 0;
677                 if (endInPara > mTextLength) endInPara = mTextLength;
678                 Arrays.fill(mCopiedBuffer, startInPara, endInPara, OBJECT_REPLACEMENT_CHARACTER);
679             }
680         }
681 
682         if (ClientFlags.icuBidiMigration()) {
683             if ((textDir == TextDirectionHeuristics.LTR
684                     || textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR
685                     || textDir == TextDirectionHeuristics.ANYRTL_LTR)
686                     && TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) {
687                 mLevels.clear();
688                 mLtrWithoutBidi = true;
689                 return;
690             }
691             final int bidiRequest;
692             if (textDir == TextDirectionHeuristics.LTR) {
693                 bidiRequest = Bidi.LTR;
694             } else if (textDir == TextDirectionHeuristics.RTL) {
695                 bidiRequest = Bidi.RTL;
696             } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) {
697                 bidiRequest = Bidi.LEVEL_DEFAULT_LTR;
698             } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
699                 bidiRequest = Bidi.LEVEL_DEFAULT_RTL;
700             } else {
701                 final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength);
702                 bidiRequest = isRtl ? Bidi.RTL : Bidi.LTR;
703             }
704             mBidi = new Bidi(mCopiedBuffer, 0, null, 0, mCopiedBuffer.length, bidiRequest);
705 
706             if (mCopiedBuffer.length > 0
707                     && mBidi.getParagraphIndex(mCopiedBuffer.length - 1) != 0) {
708                 // Historically, the MeasuredParagraph does not treat the CR letters as paragraph
709                 // breaker but ICU BiDi treats it as paragraph breaker. In the MeasureParagraph,
710                 // the given range always represents a single paragraph, so if the BiDi object has
711                 // multiple paragraph, it should contains a CR letters in the text. Using CR is not
712                 // common in Android and also it should not penalize the easy case, e.g. all LTR,
713                 // check the paragraph count here and replace the CR letters and re-calculate
714                 // BiDi again.
715                 for (int i = 0; i < mTextLength; ++i) {
716                     if (Character.isSurrogate(mCopiedBuffer[i])) {
717                         // All block separators are in BMP.
718                         continue;
719                     }
720                     if (UCharacter.getDirection(mCopiedBuffer[i])
721                             == UCharacterDirection.BLOCK_SEPARATOR) {
722                         mCopiedBuffer[i] = OBJECT_REPLACEMENT_CHARACTER;
723                     }
724                 }
725                 mBidi = new Bidi(mCopiedBuffer, 0, null, 0, mCopiedBuffer.length, bidiRequest);
726             }
727             mLevels.resize(mTextLength);
728             byte[] rawArray = mLevels.getRawArray();
729             for (int i = 0; i < mTextLength; ++i) {
730                 rawArray[i] = mBidi.getLevelAt(i);
731             }
732             mLtrWithoutBidi = false;
733             return;
734         }
735         if ((textDir == TextDirectionHeuristics.LTR
736                 || textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR
737                 || textDir == TextDirectionHeuristics.ANYRTL_LTR)
738                 && TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) {
739             mLevels.clear();
740             mParaDir = Layout.DIR_LEFT_TO_RIGHT;
741             mLtrWithoutBidi = true;
742         } else {
743             final int bidiRequest;
744             if (textDir == TextDirectionHeuristics.LTR) {
745                 bidiRequest = Layout.DIR_REQUEST_LTR;
746             } else if (textDir == TextDirectionHeuristics.RTL) {
747                 bidiRequest = Layout.DIR_REQUEST_RTL;
748             } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) {
749                 bidiRequest = Layout.DIR_REQUEST_DEFAULT_LTR;
750             } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
751                 bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL;
752             } else {
753                 final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength);
754                 bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR;
755             }
756             mLevels.resize(mTextLength);
757             mParaDir = AndroidBidi.bidi(bidiRequest, mCopiedBuffer, mLevels.getRawArray());
758             mLtrWithoutBidi = false;
759         }
760     }
761 
applyReplacementRun(@onNull ReplacementSpan replacement, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextPaint paint, @Nullable MeasuredText.Builder builder, @Nullable StyleRunCallback testCallback)762     private void applyReplacementRun(@NonNull ReplacementSpan replacement,
763                                      @IntRange(from = 0) int start,  // inclusive, in copied buffer
764                                      @IntRange(from = 0) int end,  // exclusive, in copied buffer
765                                      @NonNull TextPaint paint,
766                                      @Nullable MeasuredText.Builder builder,
767                                      @Nullable StyleRunCallback testCallback) {
768         // Use original text. Shouldn't matter.
769         // TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for
770         //       backward compatibility? or Should we initialize them for getFontMetricsInt?
771         final float width = replacement.getSize(
772                 paint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm);
773         if (builder == null) {
774             // Assigns all width to the first character. This is the same behavior as minikin.
775             mWidths.set(start, width);
776             if (end > start + 1) {
777                 Arrays.fill(mWidths.getRawArray(), start + 1, end, 0.0f);
778             }
779             mWholeWidth += width;
780         } else {
781             builder.appendReplacementRun(paint, end - start, width);
782         }
783         if (testCallback != null) {
784             testCallback.onAppendReplacementRun(paint, end - start, width);
785         }
786     }
787 
applyStyleRun(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull TextPaint paint, @Nullable LineBreakConfig config, @Nullable MeasuredText.Builder builder, @Nullable StyleRunCallback testCallback)788     private void applyStyleRun(@IntRange(from = 0) int start,  // inclusive, in copied buffer
789                                @IntRange(from = 0) int end,  // exclusive, in copied buffer
790                                @NonNull TextPaint paint,
791                                @Nullable LineBreakConfig config,
792                                @Nullable MeasuredText.Builder builder,
793                                @Nullable StyleRunCallback testCallback) {
794 
795         if (mLtrWithoutBidi) {
796             // If the whole text is LTR direction, just apply whole region.
797             if (builder == null) {
798                 // For the compatibility reasons, the letter spacing should not be dropped at the
799                 // left and right edge.
800                 int oldFlag = paint.getFlags();
801                 paint.setFlags(paint.getFlags()
802                         | (Paint.TEXT_RUN_FLAG_LEFT_EDGE | Paint.TEXT_RUN_FLAG_RIGHT_EDGE));
803                 try {
804                     mWholeWidth += paint.getTextRunAdvances(
805                             mCopiedBuffer, start, end - start, start, end - start,
806                             false /* isRtl */, mWidths.getRawArray(), start);
807                 } finally {
808                     paint.setFlags(oldFlag);
809                 }
810             } else {
811                 builder.appendStyleRun(paint, config, end - start, false /* isRtl */);
812             }
813             if (testCallback != null) {
814                 testCallback.onAppendStyleRun(paint, config, end - start, false);
815             }
816         } else {
817             // If there is multiple bidi levels, split into individual bidi level and apply style.
818             byte level = mLevels.get(start);
819             // Note that the empty text or empty range won't reach this method.
820             // Safe to search from start + 1.
821             for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) {
822                 if (levelEnd == end || mLevels.get(levelEnd) != level) {  // transition point
823                     final boolean isRtl = (level & 0x1) != 0;
824                     if (builder == null) {
825                         final int levelLength = levelEnd - levelStart;
826                         int oldFlag = paint.getFlags();
827                         paint.setFlags(paint.getFlags()
828                                 | (Paint.TEXT_RUN_FLAG_LEFT_EDGE | Paint.TEXT_RUN_FLAG_RIGHT_EDGE));
829                         try {
830                             mWholeWidth += paint.getTextRunAdvances(
831                                     mCopiedBuffer, levelStart, levelLength, levelStart, levelLength,
832                                     isRtl, mWidths.getRawArray(), levelStart);
833                         } finally {
834                             paint.setFlags(oldFlag);
835                         }
836                     } else {
837                         builder.appendStyleRun(paint, config, levelEnd - levelStart, isRtl);
838                     }
839                     if (testCallback != null) {
840                         testCallback.onAppendStyleRun(paint, config, levelEnd - levelStart, isRtl);
841                     }
842                     if (levelEnd == end) {
843                         break;
844                     }
845                     levelStart = levelEnd;
846                     level = mLevels.get(levelEnd);
847                 }
848             }
849         }
850     }
851 
applyMetricsAffectingSpan( @onNull TextPaint paint, @Nullable LineBreakConfig lineBreakConfig, @Nullable MetricAffectingSpan[] spans, @Nullable LineBreakConfigSpan[] lbcSpans, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @Nullable MeasuredText.Builder builder, @Nullable StyleRunCallback testCallback)852     private void applyMetricsAffectingSpan(
853             @NonNull TextPaint paint,
854             @Nullable LineBreakConfig lineBreakConfig,
855             @Nullable MetricAffectingSpan[] spans,
856             @Nullable LineBreakConfigSpan[] lbcSpans,
857             @IntRange(from = 0) int start,  // inclusive, in original text buffer
858             @IntRange(from = 0) int end,  // exclusive, in original text buffer
859             @Nullable MeasuredText.Builder builder,
860             @Nullable StyleRunCallback testCallback) {
861         mCachedPaint.set(paint);
862         // XXX paint should not have a baseline shift, but...
863         mCachedPaint.baselineShift = 0;
864 
865         final boolean needFontMetrics = builder != null;
866 
867         if (needFontMetrics && mCachedFm == null) {
868             mCachedFm = new Paint.FontMetricsInt();
869         }
870 
871         ReplacementSpan replacement = null;
872         if (spans != null) {
873             for (int i = 0; i < spans.length; i++) {
874                 MetricAffectingSpan span = spans[i];
875                 if (span instanceof ReplacementSpan) {
876                     // The last ReplacementSpan is effective for backward compatibility reasons.
877                     replacement = (ReplacementSpan) span;
878                 } else {
879                     // TODO: No need to call updateMeasureState for ReplacementSpan as well?
880                     span.updateMeasureState(mCachedPaint);
881                 }
882             }
883         }
884 
885         if (lbcSpans != null) {
886             mLineBreakConfigBuilder.reset(lineBreakConfig);
887             for (LineBreakConfigSpan lbcSpan : lbcSpans) {
888                 mLineBreakConfigBuilder.merge(lbcSpan.getLineBreakConfig());
889             }
890             lineBreakConfig = mLineBreakConfigBuilder.build();
891         }
892 
893         final int startInCopiedBuffer = start - mTextStart;
894         final int endInCopiedBuffer = end - mTextStart;
895 
896         if (builder != null) {
897             mCachedPaint.getFontMetricsInt(mCachedFm);
898         }
899 
900         if (replacement != null) {
901             applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer, mCachedPaint,
902                     builder, testCallback);
903         } else {
904             applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, mCachedPaint,
905                     lineBreakConfig, builder, testCallback);
906         }
907 
908         if (needFontMetrics) {
909             if (mCachedPaint.baselineShift < 0) {
910                 mCachedFm.ascent += mCachedPaint.baselineShift;
911                 mCachedFm.top += mCachedPaint.baselineShift;
912             } else {
913                 mCachedFm.descent += mCachedPaint.baselineShift;
914                 mCachedFm.bottom += mCachedPaint.baselineShift;
915             }
916 
917             mFontMetrics.append(mCachedFm.top);
918             mFontMetrics.append(mCachedFm.bottom);
919             mFontMetrics.append(mCachedFm.ascent);
920             mFontMetrics.append(mCachedFm.descent);
921         }
922     }
923 
924     /**
925      * Returns the maximum index that the accumulated width not exceeds the width.
926      *
927      * If forward=false is passed, returns the minimum index from the end instead.
928      *
929      * This only works if the MeasuredParagraph is computed with buildForMeasurement.
930      * Undefined behavior in other case.
931      */
breakText(int limit, boolean forwards, float width)932     @IntRange(from = 0) int breakText(int limit, boolean forwards, float width) {
933         float[] w = mWidths.getRawArray();
934         if (forwards) {
935             int i = 0;
936             while (i < limit) {
937                 width -= w[i];
938                 if (width < 0.0f) break;
939                 i++;
940             }
941             while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--;
942             return i;
943         } else {
944             int i = limit - 1;
945             while (i >= 0) {
946                 width -= w[i];
947                 if (width < 0.0f) break;
948                 i--;
949             }
950             while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) {
951                 i++;
952             }
953             return limit - i - 1;
954         }
955     }
956 
957     /**
958      * Returns the length of the substring.
959      *
960      * This only works if the MeasuredParagraph is computed with buildForMeasurement.
961      * Undefined behavior in other case.
962      */
measure(int start, int limit)963     @FloatRange(from = 0.0f) float measure(int start, int limit) {
964         float width = 0;
965         float[] w = mWidths.getRawArray();
966         for (int i = start; i < limit; ++i) {
967             width += w[i];
968         }
969         return width;
970     }
971 
972     /**
973      * This only works if the MeasuredParagraph is computed with buildForStaticLayout.
974      * @hide
975      */
getMemoryUsage()976     public @IntRange(from = 0) int getMemoryUsage() {
977         return mMeasuredText.getMemoryUsage();
978     }
979 }
980