MeasuredParagraph.java revision 0903665f97435d27a893d45927e4a019008cadd6
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.graphics.Rect;
25import android.text.AutoGrowArray.ByteArray;
26import android.text.AutoGrowArray.FloatArray;
27import android.text.AutoGrowArray.IntArray;
28import android.text.Layout.Directions;
29import android.text.style.MetricAffectingSpan;
30import android.text.style.ReplacementSpan;
31import android.util.Pools.SynchronizedPool;
32
33import dalvik.annotation.optimization.CriticalNative;
34
35import libcore.util.NativeAllocationRegistry;
36
37import 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 */
62public 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
68    private MeasuredParagraph() {}  // Use build static functions instead.
69
70    private static final SynchronizedPool<MeasuredParagraph> sPool = new SynchronizedPool<>(1);
71
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     */
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.
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.
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     */
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     */
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     */
184    public int getTextLength() {
185        return mTextLength;
186    }
187
188    /**
189     * Returns the characters to be measured.
190     *
191     * This is always available.
192     */
193    public @NonNull char[] getChars() {
194        return mCopiedBuffer;
195    }
196
197    /**
198     * Returns the paragraph direction.
199     *
200     * This is always available.
201     */
202    public @Layout.Direction int getParagraphDir() {
203        return mParaDir;
204    }
205
206    /**
207     * Returns the directions.
208     *
209     * This is always available.
210     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
306    public void getBounds(@NonNull Paint paint, @IntRange(from = 0) int start,
307            @IntRange(from = 0) int end, @NonNull Rect bounds) {
308        nGetBounds(mNativePtr, mCopiedBuffer, paint.getNativeInstance(), start, end,
309                paint.getBidiFlags(), bounds);
310    }
311
312    /**
313     * Generates new MeasuredParagraph for Bidi computation.
314     *
315     * If recycle is null, this returns new instance. If recycle is not null, this fills computed
316     * result to recycle and returns recycle.
317     *
318     * @param text the character sequence to be measured
319     * @param start the inclusive start offset of the target region in the text
320     * @param end the exclusive end offset of the target region in the text
321     * @param textDir the text direction
322     * @param recycle pass existing MeasuredParagraph if you want to recycle it.
323     *
324     * @return measured text
325     */
326    public static @NonNull MeasuredParagraph buildForBidi(@NonNull CharSequence text,
327                                                     @IntRange(from = 0) int start,
328                                                     @IntRange(from = 0) int end,
329                                                     @NonNull TextDirectionHeuristic textDir,
330                                                     @Nullable MeasuredParagraph recycle) {
331        final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
332        mt.resetAndAnalyzeBidi(text, start, end, textDir);
333        return mt;
334    }
335
336    /**
337     * Generates new MeasuredParagraph for measuring texts.
338     *
339     * If recycle is null, this returns new instance. If recycle is not null, this fills computed
340     * result to recycle and returns recycle.
341     *
342     * @param paint the paint to be used for rendering the text.
343     * @param text the character sequence to be measured
344     * @param start the inclusive start offset of the target region in the text
345     * @param end the exclusive end offset of the target region in the text
346     * @param textDir the text direction
347     * @param recycle pass existing MeasuredParagraph if you want to recycle it.
348     *
349     * @return measured text
350     */
351    public static @NonNull MeasuredParagraph buildForMeasurement(@NonNull TextPaint paint,
352                                                            @NonNull CharSequence text,
353                                                            @IntRange(from = 0) int start,
354                                                            @IntRange(from = 0) int end,
355                                                            @NonNull TextDirectionHeuristic textDir,
356                                                            @Nullable MeasuredParagraph recycle) {
357        final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
358        mt.resetAndAnalyzeBidi(text, start, end, textDir);
359
360        mt.mWidths.resize(mt.mTextLength);
361        if (mt.mTextLength == 0) {
362            return mt;
363        }
364
365        if (mt.mSpanned == null) {
366            // No style change by MetricsAffectingSpan. Just measure all text.
367            mt.applyMetricsAffectingSpan(
368                    paint, null /* spans */, start, end, 0 /* native static layout ptr */);
369        } else {
370            // There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
371            int spanEnd;
372            for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
373                spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class);
374                MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
375                        MetricAffectingSpan.class);
376                spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
377                mt.applyMetricsAffectingSpan(
378                        paint, spans, spanStart, spanEnd, 0 /* native static layout ptr */);
379            }
380        }
381        return mt;
382    }
383
384    /**
385     * Generates new MeasuredParagraph for StaticLayout.
386     *
387     * If recycle is null, this returns new instance. If recycle is not null, this fills computed
388     * result to recycle and returns recycle.
389     *
390     * @param paint the paint to be used for rendering the text.
391     * @param text the character sequence to be measured
392     * @param start the inclusive start offset of the target region in the text
393     * @param end the exclusive end offset of the target region in the text
394     * @param textDir the text direction
395     * @param recycle pass existing MeasuredParagraph if you want to recycle it.
396     *
397     * @return measured text
398     */
399    public static @NonNull MeasuredParagraph buildForStaticLayout(
400            @NonNull TextPaint paint,
401            @NonNull CharSequence text,
402            @IntRange(from = 0) int start,
403            @IntRange(from = 0) int end,
404            @NonNull TextDirectionHeuristic textDir,
405            boolean computeHyphenation,
406            boolean computeLayout,
407            @Nullable MeasuredParagraph recycle) {
408        final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
409        mt.resetAndAnalyzeBidi(text, start, end, textDir);
410        if (mt.mTextLength == 0) {
411            // Need to build empty native measured text for StaticLayout.
412            // TODO: Stop creating empty measured text for empty lines.
413            long nativeBuilderPtr = nInitBuilder();
414            try {
415                mt.bindNativeObject(
416                        nBuildNativeMeasuredParagraph(nativeBuilderPtr, mt.mCopiedBuffer,
417                              computeHyphenation, computeLayout));
418            } finally {
419                nFreeBuilder(nativeBuilderPtr);
420            }
421            return mt;
422        }
423
424        long nativeBuilderPtr = nInitBuilder();
425        try {
426            if (mt.mSpanned == null) {
427                // No style change by MetricsAffectingSpan. Just measure all text.
428                mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end, nativeBuilderPtr);
429                mt.mSpanEndCache.append(end);
430            } else {
431                // There may be a MetricsAffectingSpan. Split into span transitions and apply
432                // styles.
433                int spanEnd;
434                for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
435                    spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end,
436                                                             MetricAffectingSpan.class);
437                    MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
438                            MetricAffectingSpan.class);
439                    spans = TextUtils.removeEmptySpans(spans, mt.mSpanned,
440                                                       MetricAffectingSpan.class);
441                    mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd,
442                                                 nativeBuilderPtr);
443                    mt.mSpanEndCache.append(spanEnd);
444                }
445            }
446            mt.bindNativeObject(nBuildNativeMeasuredParagraph(nativeBuilderPtr, mt.mCopiedBuffer,
447                      computeHyphenation, computeLayout));
448        } finally {
449            nFreeBuilder(nativeBuilderPtr);
450        }
451
452        return mt;
453    }
454
455    /**
456     * Reset internal state and analyzes text for bidirectional runs.
457     *
458     * @param text the character sequence to be measured
459     * @param start the inclusive start offset of the target region in the text
460     * @param end the exclusive end offset of the target region in the text
461     * @param textDir the text direction
462     */
463    private void resetAndAnalyzeBidi(@NonNull CharSequence text,
464                                     @IntRange(from = 0) int start,  // inclusive
465                                     @IntRange(from = 0) int end,  // exclusive
466                                     @NonNull TextDirectionHeuristic textDir) {
467        reset();
468        mSpanned = text instanceof Spanned ? (Spanned) text : null;
469        mTextStart = start;
470        mTextLength = end - start;
471
472        if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) {
473            mCopiedBuffer = new char[mTextLength];
474        }
475        TextUtils.getChars(text, start, end, mCopiedBuffer, 0);
476
477        // Replace characters associated with ReplacementSpan to U+FFFC.
478        if (mSpanned != null) {
479            ReplacementSpan[] spans = mSpanned.getSpans(start, end, ReplacementSpan.class);
480
481            for (int i = 0; i < spans.length; i++) {
482                int startInPara = mSpanned.getSpanStart(spans[i]) - start;
483                int endInPara = mSpanned.getSpanEnd(spans[i]) - start;
484                // The span interval may be larger and must be restricted to [start, end)
485                if (startInPara < 0) startInPara = 0;
486                if (endInPara > mTextLength) endInPara = mTextLength;
487                Arrays.fill(mCopiedBuffer, startInPara, endInPara, OBJECT_REPLACEMENT_CHARACTER);
488            }
489        }
490
491        if ((textDir == TextDirectionHeuristics.LTR
492                || textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR
493                || textDir == TextDirectionHeuristics.ANYRTL_LTR)
494                && TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) {
495            mLevels.clear();
496            mParaDir = Layout.DIR_LEFT_TO_RIGHT;
497            mLtrWithoutBidi = true;
498        } else {
499            final int bidiRequest;
500            if (textDir == TextDirectionHeuristics.LTR) {
501                bidiRequest = Layout.DIR_REQUEST_LTR;
502            } else if (textDir == TextDirectionHeuristics.RTL) {
503                bidiRequest = Layout.DIR_REQUEST_RTL;
504            } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) {
505                bidiRequest = Layout.DIR_REQUEST_DEFAULT_LTR;
506            } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
507                bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL;
508            } else {
509                final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength);
510                bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR;
511            }
512            mLevels.resize(mTextLength);
513            mParaDir = AndroidBidi.bidi(bidiRequest, mCopiedBuffer, mLevels.getRawArray());
514            mLtrWithoutBidi = false;
515        }
516    }
517
518    private void applyReplacementRun(@NonNull ReplacementSpan replacement,
519                                     @IntRange(from = 0) int start,  // inclusive, in copied buffer
520                                     @IntRange(from = 0) int end,  // exclusive, in copied buffer
521                                     /* Maybe Zero */ long nativeBuilderPtr) {
522        // Use original text. Shouldn't matter.
523        // TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for
524        //       backward compatibility? or Should we initialize them for getFontMetricsInt?
525        final float width = replacement.getSize(
526                mCachedPaint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm);
527        if (nativeBuilderPtr == 0) {
528            // Assigns all width to the first character. This is the same behavior as minikin.
529            mWidths.set(start, width);
530            if (end > start + 1) {
531                Arrays.fill(mWidths.getRawArray(), start + 1, end, 0.0f);
532            }
533            mWholeWidth += width;
534        } else {
535            nAddReplacementRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
536                               width);
537        }
538    }
539
540    private void applyStyleRun(@IntRange(from = 0) int start,  // inclusive, in copied buffer
541                               @IntRange(from = 0) int end,  // exclusive, in copied buffer
542                               /* Maybe Zero */ long nativeBuilderPtr) {
543
544        if (mLtrWithoutBidi) {
545            // If the whole text is LTR direction, just apply whole region.
546            if (nativeBuilderPtr == 0) {
547                mWholeWidth += mCachedPaint.getTextRunAdvances(
548                        mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */,
549                        mWidths.getRawArray(), start);
550            } else {
551                nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
552                        false /* isRtl */);
553            }
554        } else {
555            // If there is multiple bidi levels, split into individual bidi level and apply style.
556            byte level = mLevels.get(start);
557            // Note that the empty text or empty range won't reach this method.
558            // Safe to search from start + 1.
559            for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) {
560                if (levelEnd == end || mLevels.get(levelEnd) != level) {  // transition point
561                    final boolean isRtl = (level & 0x1) != 0;
562                    if (nativeBuilderPtr == 0) {
563                        final int levelLength = levelEnd - levelStart;
564                        mWholeWidth += mCachedPaint.getTextRunAdvances(
565                                mCopiedBuffer, levelStart, levelLength, levelStart, levelLength,
566                                isRtl, mWidths.getRawArray(), levelStart);
567                    } else {
568                        nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), levelStart,
569                                levelEnd, isRtl);
570                    }
571                    if (levelEnd == end) {
572                        break;
573                    }
574                    levelStart = levelEnd;
575                    level = mLevels.get(levelEnd);
576                }
577            }
578        }
579    }
580
581    private void applyMetricsAffectingSpan(
582            @NonNull TextPaint paint,
583            @Nullable MetricAffectingSpan[] spans,
584            @IntRange(from = 0) int start,  // inclusive, in original text buffer
585            @IntRange(from = 0) int end,  // exclusive, in original text buffer
586            /* Maybe Zero */ long nativeBuilderPtr) {
587        mCachedPaint.set(paint);
588        // XXX paint should not have a baseline shift, but...
589        mCachedPaint.baselineShift = 0;
590
591        final boolean needFontMetrics = nativeBuilderPtr != 0;
592
593        if (needFontMetrics && mCachedFm == null) {
594            mCachedFm = new Paint.FontMetricsInt();
595        }
596
597        ReplacementSpan replacement = null;
598        if (spans != null) {
599            for (int i = 0; i < spans.length; i++) {
600                MetricAffectingSpan span = spans[i];
601                if (span instanceof ReplacementSpan) {
602                    // The last ReplacementSpan is effective for backward compatibility reasons.
603                    replacement = (ReplacementSpan) span;
604                } else {
605                    // TODO: No need to call updateMeasureState for ReplacementSpan as well?
606                    span.updateMeasureState(mCachedPaint);
607                }
608            }
609        }
610
611        final int startInCopiedBuffer = start - mTextStart;
612        final int endInCopiedBuffer = end - mTextStart;
613
614        if (nativeBuilderPtr != 0) {
615            mCachedPaint.getFontMetricsInt(mCachedFm);
616        }
617
618        if (replacement != null) {
619            applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer,
620                                nativeBuilderPtr);
621        } else {
622            applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, nativeBuilderPtr);
623        }
624
625        if (needFontMetrics) {
626            if (mCachedPaint.baselineShift < 0) {
627                mCachedFm.ascent += mCachedPaint.baselineShift;
628                mCachedFm.top += mCachedPaint.baselineShift;
629            } else {
630                mCachedFm.descent += mCachedPaint.baselineShift;
631                mCachedFm.bottom += mCachedPaint.baselineShift;
632            }
633
634            mFontMetrics.append(mCachedFm.top);
635            mFontMetrics.append(mCachedFm.bottom);
636            mFontMetrics.append(mCachedFm.ascent);
637            mFontMetrics.append(mCachedFm.descent);
638        }
639    }
640
641    /**
642     * Returns the maximum index that the accumulated width not exceeds the width.
643     *
644     * If forward=false is passed, returns the minimum index from the end instead.
645     *
646     * This only works if the MeasuredParagraph is computed with buildForMeasurement.
647     * Undefined behavior in other case.
648     */
649    @IntRange(from = 0) int breakText(int limit, boolean forwards, float width) {
650        float[] w = mWidths.getRawArray();
651        if (forwards) {
652            int i = 0;
653            while (i < limit) {
654                width -= w[i];
655                if (width < 0.0f) break;
656                i++;
657            }
658            while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--;
659            return i;
660        } else {
661            int i = limit - 1;
662            while (i >= 0) {
663                width -= w[i];
664                if (width < 0.0f) break;
665                i--;
666            }
667            while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) {
668                i++;
669            }
670            return limit - i - 1;
671        }
672    }
673
674    /**
675     * Returns the length of the substring.
676     *
677     * This only works if the MeasuredParagraph is computed with buildForMeasurement.
678     * Undefined behavior in other case.
679     */
680    @FloatRange(from = 0.0f) float measure(int start, int limit) {
681        float width = 0;
682        float[] w = mWidths.getRawArray();
683        for (int i = start; i < limit; ++i) {
684            width += w[i];
685        }
686        return width;
687    }
688
689    /**
690     * This only works if the MeasuredParagraph is computed with buildForStaticLayout.
691     */
692    public @IntRange(from = 0) int getMemoryUsage() {
693        return nGetMemoryUsage(mNativePtr);
694    }
695
696    private static native /* Non Zero */ long nInitBuilder();
697
698    /**
699     * Apply style to make native measured text.
700     *
701     * @param nativeBuilderPtr The native MeasuredParagraph builder pointer.
702     * @param paintPtr The native paint pointer to be applied.
703     * @param start The start offset in the copied buffer.
704     * @param end The end offset in the copied buffer.
705     * @param isRtl True if the text is RTL.
706     */
707    private static native void nAddStyleRun(/* Non Zero */ long nativeBuilderPtr,
708                                            /* Non Zero */ long paintPtr,
709                                            @IntRange(from = 0) int start,
710                                            @IntRange(from = 0) int end,
711                                            boolean isRtl);
712
713    /**
714     * Apply ReplacementRun to make native measured text.
715     *
716     * @param nativeBuilderPtr The native MeasuredParagraph builder pointer.
717     * @param paintPtr The native paint pointer to be applied.
718     * @param start The start offset in the copied buffer.
719     * @param end The end offset in the copied buffer.
720     * @param width The width of the replacement.
721     */
722    private static native void nAddReplacementRun(/* Non Zero */ long nativeBuilderPtr,
723                                                  /* Non Zero */ long paintPtr,
724                                                  @IntRange(from = 0) int start,
725                                                  @IntRange(from = 0) int end,
726                                                  @FloatRange(from = 0) float width);
727
728    private static native long nBuildNativeMeasuredParagraph(/* Non Zero */ long nativeBuilderPtr,
729                                                 @NonNull char[] text,
730                                                 boolean computeHyphenation,
731                                                 boolean computeLayout);
732
733    private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr);
734
735    @CriticalNative
736    private static native float nGetWidth(/* Non Zero */ long nativePtr,
737                                         @IntRange(from = 0) int start,
738                                         @IntRange(from = 0) int end);
739
740    @CriticalNative
741    private static native /* Non Zero */ long nGetReleaseFunc();
742
743    @CriticalNative
744    private static native int nGetMemoryUsage(/* Non Zero */ long nativePtr);
745
746    private static native void nGetBounds(long nativePtr, char[] buf, long paintPtr, int start,
747            int end, int bidiFlag, Rect rect);
748}
749