1 /*
2  * Copyright (C) 2017 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.IntDef;
21 import android.annotation.IntRange;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.graphics.Rect;
25 import android.text.style.MetricAffectingSpan;
26 
27 import com.android.internal.util.Preconditions;
28 
29 import java.lang.annotation.Retention;
30 import java.lang.annotation.RetentionPolicy;
31 import java.util.ArrayList;
32 import java.util.Objects;
33 
34 /**
35  * A text which has the character metrics data.
36  *
37  * A text object that contains the character metrics data and can be used to improve the performance
38  * of text layout operations. When a PrecomputedText is created with a given {@link CharSequence},
39  * it will measure the text metrics during the creation. This PrecomputedText instance can be set on
40  * {@link android.widget.TextView} or {@link StaticLayout}. Since the text layout information will
41  * be included in this instance, {@link android.widget.TextView} or {@link StaticLayout} will not
42  * have to recalculate this information.
43  *
44  * Note that the {@link PrecomputedText} created from different parameters of the target {@link
45  * android.widget.TextView} will be rejected internally and compute the text layout again with the
46  * current {@link android.widget.TextView} parameters.
47  *
48  * <pre>
49  * An example usage is:
50  * <code>
51  *  static void asyncSetText(TextView textView, final String longString, Executor bgExecutor) {
52  *      // construct precompute related parameters using the TextView that we will set the text on.
53  *      final PrecomputedText.Params params = textView.getTextMetricsParams();
54  *      final Reference textViewRef = new WeakReference<>(textView);
55  *      bgExecutor.submit(() -> {
56  *          TextView textView = textViewRef.get();
57  *          if (textView == null) return;
58  *          final PrecomputedText precomputedText = PrecomputedText.create(longString, params);
59  *          textView.post(() -> {
60  *              TextView textView = textViewRef.get();
61  *              if (textView == null) return;
62  *              textView.setText(precomputedText);
63  *          });
64  *      });
65  *  }
66  * </code>
67  * </pre>
68  *
69  * Note that the {@link PrecomputedText} created from different parameters of the target
70  * {@link android.widget.TextView} will be rejected.
71  *
72  * Note that any {@link android.text.NoCopySpan} attached to the original text won't be passed to
73  * PrecomputedText.
74  */
75 public class PrecomputedText implements Spannable {
76     private static final char LINE_FEED = '\n';
77 
78     /**
79      * The information required for building {@link PrecomputedText}.
80      *
81      * Contains information required for precomputing text measurement metadata, so it can be done
82      * in isolation of a {@link android.widget.TextView} or {@link StaticLayout}, when final layout
83      * constraints are not known.
84      */
85     public static final class Params {
86         // The TextPaint used for measurement.
87         private final @NonNull TextPaint mPaint;
88 
89         // The requested text direction.
90         private final @NonNull TextDirectionHeuristic mTextDir;
91 
92         // The break strategy for this measured text.
93         private final @Layout.BreakStrategy int mBreakStrategy;
94 
95         // The hyphenation frequency for this measured text.
96         private final @Layout.HyphenationFrequency int mHyphenationFrequency;
97 
98         /**
99          * A builder for creating {@link Params}.
100          */
101         public static class Builder {
102             // The TextPaint used for measurement.
103             private final @NonNull TextPaint mPaint;
104 
105             // The requested text direction.
106             private TextDirectionHeuristic mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
107 
108             // The break strategy for this measured text.
109             private @Layout.BreakStrategy int mBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
110 
111             // The hyphenation frequency for this measured text.
112             private @Layout.HyphenationFrequency int mHyphenationFrequency =
113                     Layout.HYPHENATION_FREQUENCY_NORMAL;
114 
115             /**
116              * Builder constructor.
117              *
118              * @param paint the paint to be used for drawing
119              */
Builder(@onNull TextPaint paint)120             public Builder(@NonNull TextPaint paint) {
121                 mPaint = paint;
122             }
123 
124             /**
125              * Builder constructor from existing params.
126              */
Builder(@onNull Params params)127             public Builder(@NonNull Params params) {
128                 mPaint = params.mPaint;
129                 mTextDir = params.mTextDir;
130                 mBreakStrategy = params.mBreakStrategy;
131                 mHyphenationFrequency = params.mHyphenationFrequency;
132             }
133 
134             /**
135              * Set the line break strategy.
136              *
137              * The default value is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}.
138              *
139              * @param strategy the break strategy
140              * @return this builder, useful for chaining
141              * @see StaticLayout.Builder#setBreakStrategy
142              * @see android.widget.TextView#setBreakStrategy
143              */
setBreakStrategy(@ayout.BreakStrategy int strategy)144             public Builder setBreakStrategy(@Layout.BreakStrategy int strategy) {
145                 mBreakStrategy = strategy;
146                 return this;
147             }
148 
149             /**
150              * Set the hyphenation frequency.
151              *
152              * The default value is {@link Layout#HYPHENATION_FREQUENCY_NORMAL}.
153              *
154              * @param frequency the hyphenation frequency
155              * @return this builder, useful for chaining
156              * @see StaticLayout.Builder#setHyphenationFrequency
157              * @see android.widget.TextView#setHyphenationFrequency
158              */
setHyphenationFrequency(@ayout.HyphenationFrequency int frequency)159             public Builder setHyphenationFrequency(@Layout.HyphenationFrequency int frequency) {
160                 mHyphenationFrequency = frequency;
161                 return this;
162             }
163 
164             /**
165              * Set the text direction heuristic.
166              *
167              * The default value is {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}.
168              *
169              * @param textDir the text direction heuristic for resolving bidi behavior
170              * @return this builder, useful for chaining
171              * @see StaticLayout.Builder#setTextDirection
172              */
setTextDirection(@onNull TextDirectionHeuristic textDir)173             public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) {
174                 mTextDir = textDir;
175                 return this;
176             }
177 
178             /**
179              * Build the {@link Params}.
180              *
181              * @return the layout parameter
182              */
build()183             public @NonNull Params build() {
184                 return new Params(mPaint, mTextDir, mBreakStrategy, mHyphenationFrequency);
185             }
186         }
187 
188         // This is public hidden for internal use.
189         // For the external developers, use Builder instead.
190         /** @hide */
Params(@onNull TextPaint paint, @NonNull TextDirectionHeuristic textDir, @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency)191         public Params(@NonNull TextPaint paint, @NonNull TextDirectionHeuristic textDir,
192                 @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency) {
193             mPaint = paint;
194             mTextDir = textDir;
195             mBreakStrategy = strategy;
196             mHyphenationFrequency = frequency;
197         }
198 
199         /**
200          * Returns the {@link TextPaint} for this text.
201          *
202          * @return A {@link TextPaint}
203          */
getTextPaint()204         public @NonNull TextPaint getTextPaint() {
205             return mPaint;
206         }
207 
208         /**
209          * Returns the {@link TextDirectionHeuristic} for this text.
210          *
211          * @return A {@link TextDirectionHeuristic}
212          */
getTextDirection()213         public @NonNull TextDirectionHeuristic getTextDirection() {
214             return mTextDir;
215         }
216 
217         /**
218          * Returns the break strategy for this text.
219          *
220          * @return A line break strategy
221          */
getBreakStrategy()222         public @Layout.BreakStrategy int getBreakStrategy() {
223             return mBreakStrategy;
224         }
225 
226         /**
227          * Returns the hyphenation frequency for this text.
228          *
229          * @return A hyphenation frequency
230          */
getHyphenationFrequency()231         public @Layout.HyphenationFrequency int getHyphenationFrequency() {
232             return mHyphenationFrequency;
233         }
234 
235         /** @hide */
236         @IntDef(value = { UNUSABLE, NEED_RECOMPUTE, USABLE })
237         @Retention(RetentionPolicy.SOURCE)
238         public @interface CheckResultUsableResult {}
239 
240         /**
241          * Constant for returning value of checkResultUsable indicating that given parameter is not
242          * compatible.
243          * @hide
244          */
245         public static final int UNUSABLE = 0;
246 
247         /**
248          * Constant for returning value of checkResultUsable indicating that given parameter is not
249          * compatible but partially usable for creating new PrecomputedText.
250          * @hide
251          */
252         public static final int NEED_RECOMPUTE = 1;
253 
254         /**
255          * Constant for returning value of checkResultUsable indicating that given parameter is
256          * compatible.
257          * @hide
258          */
259         public static final int USABLE = 2;
260 
261         /** @hide */
checkResultUsable(@onNull TextPaint paint, @NonNull TextDirectionHeuristic textDir, @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency)262         public @CheckResultUsableResult int checkResultUsable(@NonNull TextPaint paint,
263                 @NonNull TextDirectionHeuristic textDir, @Layout.BreakStrategy int strategy,
264                 @Layout.HyphenationFrequency int frequency) {
265             if (mBreakStrategy == strategy && mHyphenationFrequency == frequency
266                     && mPaint.equalsForTextMeasurement(paint)) {
267                 return mTextDir == textDir ? USABLE : NEED_RECOMPUTE;
268             } else {
269                 return UNUSABLE;
270             }
271         }
272 
273         /**
274          * Check if the same text layout.
275          *
276          * @return true if this and the given param result in the same text layout
277          */
278         @Override
equals(@ullable Object o)279         public boolean equals(@Nullable Object o) {
280             if (o == this) {
281                 return true;
282             }
283             if (o == null || !(o instanceof Params)) {
284                 return false;
285             }
286             Params param = (Params) o;
287             return checkResultUsable(param.mPaint, param.mTextDir, param.mBreakStrategy,
288                     param.mHyphenationFrequency) == Params.USABLE;
289         }
290 
291         @Override
hashCode()292         public int hashCode() {
293             // TODO: implement MinikinPaint::hashCode and use it to keep consistency with equals.
294             return Objects.hash(mPaint.getTextSize(), mPaint.getTextScaleX(), mPaint.getTextSkewX(),
295                     mPaint.getLetterSpacing(), mPaint.getWordSpacing(), mPaint.getFlags(),
296                     mPaint.getTextLocales(), mPaint.getTypeface(),
297                     mPaint.getFontVariationSettings(), mPaint.isElegantTextHeight(), mTextDir,
298                     mBreakStrategy, mHyphenationFrequency);
299         }
300 
301         @Override
toString()302         public String toString() {
303             return "{"
304                 + "textSize=" + mPaint.getTextSize()
305                 + ", textScaleX=" + mPaint.getTextScaleX()
306                 + ", textSkewX=" + mPaint.getTextSkewX()
307                 + ", letterSpacing=" + mPaint.getLetterSpacing()
308                 + ", textLocale=" + mPaint.getTextLocales()
309                 + ", typeface=" + mPaint.getTypeface()
310                 + ", variationSettings=" + mPaint.getFontVariationSettings()
311                 + ", elegantTextHeight=" + mPaint.isElegantTextHeight()
312                 + ", textDir=" + mTextDir
313                 + ", breakStrategy=" + mBreakStrategy
314                 + ", hyphenationFrequency=" + mHyphenationFrequency
315                 + "}";
316         }
317     };
318 
319     /** @hide */
320     public static class ParagraphInfo {
321         public final @IntRange(from = 0) int paragraphEnd;
322         public final @NonNull MeasuredParagraph measured;
323 
324         /**
325          * @param paraEnd the end offset of this paragraph
326          * @param measured a measured paragraph
327          */
ParagraphInfo(@ntRangefrom = 0) int paraEnd, @NonNull MeasuredParagraph measured)328         public ParagraphInfo(@IntRange(from = 0) int paraEnd, @NonNull MeasuredParagraph measured) {
329             this.paragraphEnd = paraEnd;
330             this.measured = measured;
331         }
332     };
333 
334 
335     // The original text.
336     private final @NonNull SpannableString mText;
337 
338     // The inclusive start offset of the measuring target.
339     private final @IntRange(from = 0) int mStart;
340 
341     // The exclusive end offset of the measuring target.
342     private final @IntRange(from = 0) int mEnd;
343 
344     private final @NonNull Params mParams;
345 
346     // The list of measured paragraph info.
347     private final @NonNull ParagraphInfo[] mParagraphInfo;
348 
349     /**
350      * Create a new {@link PrecomputedText} which will pre-compute text measurement and glyph
351      * positioning information.
352      * <p>
353      * This can be expensive, so computing this on a background thread before your text will be
354      * presented can save work on the UI thread.
355      * </p>
356      *
357      * Note that any {@link android.text.NoCopySpan} attached to the text won't be passed to the
358      * created PrecomputedText.
359      *
360      * @param text the text to be measured
361      * @param params parameters that define how text will be precomputed
362      * @return A {@link PrecomputedText}
363      */
create(@onNull CharSequence text, @NonNull Params params)364     public static PrecomputedText create(@NonNull CharSequence text, @NonNull Params params) {
365         ParagraphInfo[] paraInfo = null;
366         if (text instanceof PrecomputedText) {
367             final PrecomputedText hintPct = (PrecomputedText) text;
368             final PrecomputedText.Params hintParams = hintPct.getParams();
369             final @Params.CheckResultUsableResult int checkResult =
370                     hintParams.checkResultUsable(params.mPaint, params.mTextDir,
371                             params.mBreakStrategy, params.mHyphenationFrequency);
372             switch (checkResult) {
373                 case Params.USABLE:
374                     return hintPct;
375                 case Params.NEED_RECOMPUTE:
376                     // To be able to use PrecomputedText for new params, at least break strategy and
377                     // hyphenation frequency must be the same.
378                     if (params.getBreakStrategy() == hintParams.getBreakStrategy()
379                             && params.getHyphenationFrequency()
380                                 == hintParams.getHyphenationFrequency()) {
381                         paraInfo = createMeasuredParagraphsFromPrecomputedText(
382                                 hintPct, params, true /* compute layout */);
383                     }
384                     break;
385                 case Params.UNUSABLE:
386                     // Unable to use anything in PrecomputedText. Create PrecomputedText as the
387                     // normal text input.
388             }
389 
390         }
391         if (paraInfo == null) {
392             paraInfo = createMeasuredParagraphs(
393                     text, params, 0, text.length(), true /* computeLayout */);
394         }
395         return new PrecomputedText(text, 0, text.length(), params, paraInfo);
396     }
397 
createMeasuredParagraphsFromPrecomputedText( @onNull PrecomputedText pct, @NonNull Params params, boolean computeLayout)398     private static ParagraphInfo[] createMeasuredParagraphsFromPrecomputedText(
399             @NonNull PrecomputedText pct, @NonNull Params params, boolean computeLayout) {
400         final boolean needHyphenation = params.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE
401                 && params.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE;
402         ArrayList<ParagraphInfo> result = new ArrayList<>();
403         for (int i = 0; i < pct.getParagraphCount(); ++i) {
404             final int paraStart = pct.getParagraphStart(i);
405             final int paraEnd = pct.getParagraphEnd(i);
406             result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout(
407                     params.getTextPaint(), pct, paraStart, paraEnd, params.getTextDirection(),
408                     needHyphenation, computeLayout, pct.getMeasuredParagraph(i),
409                     null /* no recycle */)));
410         }
411         return result.toArray(new ParagraphInfo[result.size()]);
412     }
413 
414     /** @hide */
createMeasuredParagraphs( @onNull CharSequence text, @NonNull Params params, @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean computeLayout)415     public static ParagraphInfo[] createMeasuredParagraphs(
416             @NonNull CharSequence text, @NonNull Params params,
417             @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean computeLayout) {
418         ArrayList<ParagraphInfo> result = new ArrayList<>();
419 
420         Preconditions.checkNotNull(text);
421         Preconditions.checkNotNull(params);
422         final boolean needHyphenation = params.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE
423                 && params.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE;
424 
425         int paraEnd = 0;
426         for (int paraStart = start; paraStart < end; paraStart = paraEnd) {
427             paraEnd = TextUtils.indexOf(text, LINE_FEED, paraStart, end);
428             if (paraEnd < 0) {
429                 // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph
430                 // end.
431                 paraEnd = end;
432             } else {
433                 paraEnd++;  // Includes LINE_FEED(U+000A) to the prev paragraph.
434             }
435 
436             result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout(
437                     params.getTextPaint(), text, paraStart, paraEnd, params.getTextDirection(),
438                     needHyphenation, computeLayout, null /* no hint */,
439                     null /* no recycle */)));
440         }
441         return result.toArray(new ParagraphInfo[result.size()]);
442     }
443 
444     // Use PrecomputedText.create instead.
PrecomputedText(@onNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull Params params, @NonNull ParagraphInfo[] paraInfo)445     private PrecomputedText(@NonNull CharSequence text, @IntRange(from = 0) int start,
446             @IntRange(from = 0) int end, @NonNull Params params,
447             @NonNull ParagraphInfo[] paraInfo) {
448         mText = new SpannableString(text, true /* ignoreNoCopySpan */);
449         mStart = start;
450         mEnd = end;
451         mParams = params;
452         mParagraphInfo = paraInfo;
453     }
454 
455     /**
456      * Return the underlying text.
457      * @hide
458      */
getText()459     public @NonNull CharSequence getText() {
460         return mText;
461     }
462 
463     /**
464      * Returns the inclusive start offset of measured region.
465      * @hide
466      */
getStart()467     public @IntRange(from = 0) int getStart() {
468         return mStart;
469     }
470 
471     /**
472      * Returns the exclusive end offset of measured region.
473      * @hide
474      */
getEnd()475     public @IntRange(from = 0) int getEnd() {
476         return mEnd;
477     }
478 
479     /**
480      * Returns the layout parameters used to measure this text.
481      */
getParams()482     public @NonNull Params getParams() {
483         return mParams;
484     }
485 
486     /**
487      * Returns the count of paragraphs.
488      */
getParagraphCount()489     public @IntRange(from = 0) int getParagraphCount() {
490         return mParagraphInfo.length;
491     }
492 
493     /**
494      * Returns the paragraph start offset of the text.
495      */
getParagraphStart(@ntRangefrom = 0) int paraIndex)496     public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) {
497         Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
498         return paraIndex == 0 ? mStart : getParagraphEnd(paraIndex - 1);
499     }
500 
501     /**
502      * Returns the paragraph end offset of the text.
503      */
getParagraphEnd(@ntRangefrom = 0) int paraIndex)504     public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) {
505         Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
506         return mParagraphInfo[paraIndex].paragraphEnd;
507     }
508 
509     /** @hide */
getMeasuredParagraph(@ntRangefrom = 0) int paraIndex)510     public @NonNull MeasuredParagraph getMeasuredParagraph(@IntRange(from = 0) int paraIndex) {
511         return mParagraphInfo[paraIndex].measured;
512     }
513 
514     /** @hide */
getParagraphInfo()515     public @NonNull ParagraphInfo[] getParagraphInfo() {
516         return mParagraphInfo;
517     }
518 
519     /**
520      * Returns true if the given TextPaint gives the same result of text layout for this text.
521      * @hide
522      */
checkResultUsable(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir, @NonNull TextPaint paint, @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency)523     public @Params.CheckResultUsableResult int checkResultUsable(@IntRange(from = 0) int start,
524             @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir,
525             @NonNull TextPaint paint, @Layout.BreakStrategy int strategy,
526             @Layout.HyphenationFrequency int frequency) {
527         if (mStart != start || mEnd != end) {
528             return Params.UNUSABLE;
529         } else {
530             return mParams.checkResultUsable(paint, textDir, strategy, frequency);
531         }
532     }
533 
534     /** @hide */
findParaIndex(@ntRangefrom = 0) int pos)535     public int findParaIndex(@IntRange(from = 0) int pos) {
536         // TODO: Maybe good to remove paragraph concept from PrecomputedText and add substring
537         //       layout support to StaticLayout.
538         for (int i = 0; i < mParagraphInfo.length; ++i) {
539             if (pos < mParagraphInfo[i].paragraphEnd) {
540                 return i;
541             }
542         }
543         throw new IndexOutOfBoundsException(
544             "pos must be less than " + mParagraphInfo[mParagraphInfo.length - 1].paragraphEnd
545             + ", gave " + pos);
546     }
547 
548     /**
549      * Returns text width for the given range.
550      * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise
551      * IllegalArgumentException will be thrown.
552      *
553      * @param start the inclusive start offset in the text
554      * @param end the exclusive end offset in the text
555      * @return the text width
556      * @throws IllegalArgumentException if start and end offset are in the different paragraph.
557      */
getWidth(@ntRangefrom = 0) int start, @IntRange(from = 0) int end)558     public @FloatRange(from = 0) float getWidth(@IntRange(from = 0) int start,
559             @IntRange(from = 0) int end) {
560         Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset");
561         Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset");
562         Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset");
563 
564         if (start == end) {
565             return 0;
566         }
567         final int paraIndex = findParaIndex(start);
568         final int paraStart = getParagraphStart(paraIndex);
569         final int paraEnd = getParagraphEnd(paraIndex);
570         if (start < paraStart || paraEnd < end) {
571             throw new IllegalArgumentException("Cannot measured across the paragraph:"
572                 + "para: (" + paraStart + ", " + paraEnd + "), "
573                 + "request: (" + start + ", " + end + ")");
574         }
575         return getMeasuredParagraph(paraIndex).getWidth(start - paraStart, end - paraStart);
576     }
577 
578     /**
579      * Retrieves the text bounding box for the given range.
580      * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise
581      * IllegalArgumentException will be thrown.
582      *
583      * @param start the inclusive start offset in the text
584      * @param end the exclusive end offset in the text
585      * @param bounds the output rectangle
586      * @throws IllegalArgumentException if start and end offset are in the different paragraph.
587      */
getBounds(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull Rect bounds)588     public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
589             @NonNull Rect bounds) {
590         Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset");
591         Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset");
592         Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset");
593         Preconditions.checkNotNull(bounds);
594         if (start == end) {
595             bounds.set(0, 0, 0, 0);
596             return;
597         }
598         final int paraIndex = findParaIndex(start);
599         final int paraStart = getParagraphStart(paraIndex);
600         final int paraEnd = getParagraphEnd(paraIndex);
601         if (start < paraStart || paraEnd < end) {
602             throw new IllegalArgumentException("Cannot measured across the paragraph:"
603                 + "para: (" + paraStart + ", " + paraEnd + "), "
604                 + "request: (" + start + ", " + end + ")");
605         }
606         getMeasuredParagraph(paraIndex).getBounds(start - paraStart, end - paraStart, bounds);
607     }
608 
609     /**
610      * Returns a width of a character at offset
611      *
612      * @param offset an offset of the text.
613      * @return a width of the character.
614      * @hide
615      */
getCharWidthAt(@ntRangefrom = 0) int offset)616     public float getCharWidthAt(@IntRange(from = 0) int offset) {
617         Preconditions.checkArgument(0 <= offset && offset < mText.length(), "invalid offset");
618         final int paraIndex = findParaIndex(offset);
619         final int paraStart = getParagraphStart(paraIndex);
620         final int paraEnd = getParagraphEnd(paraIndex);
621         return getMeasuredParagraph(paraIndex).getCharWidthAt(offset - paraStart);
622     }
623 
624     /**
625      * Returns the size of native PrecomputedText memory usage.
626      *
627      * Note that this is not guaranteed to be accurate. Must be used only for testing purposes.
628      * @hide
629      */
getMemoryUsage()630     public int getMemoryUsage() {
631         int r = 0;
632         for (int i = 0; i < getParagraphCount(); ++i) {
633             r += getMeasuredParagraph(i).getMemoryUsage();
634         }
635         return r;
636     }
637 
638     ///////////////////////////////////////////////////////////////////////////////////////////////
639     // Spannable overrides
640     //
641     // Do not allow to modify MetricAffectingSpan
642 
643     /**
644      * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified.
645      */
646     @Override
setSpan(Object what, int start, int end, int flags)647     public void setSpan(Object what, int start, int end, int flags) {
648         if (what instanceof MetricAffectingSpan) {
649             throw new IllegalArgumentException(
650                     "MetricAffectingSpan can not be set to PrecomputedText.");
651         }
652         mText.setSpan(what, start, end, flags);
653     }
654 
655     /**
656      * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified.
657      */
658     @Override
removeSpan(Object what)659     public void removeSpan(Object what) {
660         if (what instanceof MetricAffectingSpan) {
661             throw new IllegalArgumentException(
662                     "MetricAffectingSpan can not be removed from PrecomputedText.");
663         }
664         mText.removeSpan(what);
665     }
666 
667     ///////////////////////////////////////////////////////////////////////////////////////////////
668     // Spanned overrides
669     //
670     // Just proxy for underlying mText if appropriate.
671 
672     @Override
getSpans(int start, int end, Class<T> type)673     public <T> T[] getSpans(int start, int end, Class<T> type) {
674         return mText.getSpans(start, end, type);
675     }
676 
677     @Override
getSpanStart(Object tag)678     public int getSpanStart(Object tag) {
679         return mText.getSpanStart(tag);
680     }
681 
682     @Override
getSpanEnd(Object tag)683     public int getSpanEnd(Object tag) {
684         return mText.getSpanEnd(tag);
685     }
686 
687     @Override
getSpanFlags(Object tag)688     public int getSpanFlags(Object tag) {
689         return mText.getSpanFlags(tag);
690     }
691 
692     @Override
nextSpanTransition(int start, int limit, Class type)693     public int nextSpanTransition(int start, int limit, Class type) {
694         return mText.nextSpanTransition(start, limit, type);
695     }
696 
697     ///////////////////////////////////////////////////////////////////////////////////////////////
698     // CharSequence overrides.
699     //
700     // Just proxy for underlying mText.
701 
702     @Override
length()703     public int length() {
704         return mText.length();
705     }
706 
707     @Override
charAt(int index)708     public char charAt(int index) {
709         return mText.charAt(index);
710     }
711 
712     @Override
subSequence(int start, int end)713     public CharSequence subSequence(int start, int end) {
714         return PrecomputedText.create(mText.subSequence(start, end), mParams);
715     }
716 
717     @Override
toString()718     public String toString() {
719         return mText.toString();
720     }
721 }
722