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