Home | History | Annotate | Download | only in text
      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 
     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(@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      */
    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      */
    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      */
    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      */
    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 
    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 
    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 
    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      */
    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      */
    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      */
    691     public @IntRange(from = 0) int getMemoryUsage() {
    692         return nGetMemoryUsage(mNativePtr);
    693     }
    694 
    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      */
    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      */
    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 
    727     private static native long nBuildNativeMeasuredParagraph(/* Non Zero */ long nativeBuilderPtr,
    728                                                  @NonNull char[] text,
    729                                                  boolean computeHyphenation,
    730                                                  boolean computeLayout);
    731 
    732     private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr);
    733 
    734     @CriticalNative
    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
    740     private static native /* Non Zero */ long nGetReleaseFunc();
    741 
    742     @CriticalNative
    743     private static native int nGetMemoryUsage(/* Non Zero */ long nativePtr);
    744 
    745     private static native void nGetBounds(long nativePtr, char[] buf, int start, int end,
    746             Rect rect);
    747 }
    748