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