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