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