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