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             return Objects.hash(mPaint.getTextSize(), mPaint.getTextScaleX(), mPaint.getTextSkewX(),
333                     mPaint.getLetterSpacing(), mPaint.getWordSpacing(), mPaint.getFlags(),
334                     mPaint.getTextLocales(), mPaint.getTypeface(),
335                     mPaint.getFontVariationSettings(), mPaint.isElegantTextHeight(), mTextDir,
336                     mBreakStrategy, mHyphenationFrequency,
337                     LineBreakConfig.getResolvedLineBreakStyle(mLineBreakConfig),
338                     LineBreakConfig.getResolvedLineBreakWordStyle(mLineBreakConfig));
339         }
340 
341         @Override
toString()342         public String toString() {
343             return "{"
344                 + "textSize=" + mPaint.getTextSize()
345                 + ", textScaleX=" + mPaint.getTextScaleX()
346                 + ", textSkewX=" + mPaint.getTextSkewX()
347                 + ", letterSpacing=" + mPaint.getLetterSpacing()
348                 + ", textLocale=" + mPaint.getTextLocales()
349                 + ", typeface=" + mPaint.getTypeface()
350                 + ", variationSettings=" + mPaint.getFontVariationSettings()
351                 + ", elegantTextHeight=" + mPaint.isElegantTextHeight()
352                 + ", textDir=" + mTextDir
353                 + ", breakStrategy=" + mBreakStrategy
354                 + ", hyphenationFrequency=" + mHyphenationFrequency
355                 + ", lineBreakStyle=" + LineBreakConfig.getResolvedLineBreakStyle(mLineBreakConfig)
356                 + ", lineBreakWordStyle="
357                     + LineBreakConfig.getResolvedLineBreakWordStyle(mLineBreakConfig)
358                 + "}";
359         }
360     };
361 
362     /** @hide */
363     public static class ParagraphInfo {
364         public final @IntRange(from = 0) int paragraphEnd;
365         public final @NonNull MeasuredParagraph measured;
366 
367         /**
368          * @param paraEnd the end offset of this paragraph
369          * @param measured a measured paragraph
370          */
ParagraphInfo(@ntRangefrom = 0) int paraEnd, @NonNull MeasuredParagraph measured)371         public ParagraphInfo(@IntRange(from = 0) int paraEnd, @NonNull MeasuredParagraph measured) {
372             this.paragraphEnd = paraEnd;
373             this.measured = measured;
374         }
375     };
376 
377 
378     // The original text.
379     private final @NonNull SpannableString mText;
380 
381     // The inclusive start offset of the measuring target.
382     private final @IntRange(from = 0) int mStart;
383 
384     // The exclusive end offset of the measuring target.
385     private final @IntRange(from = 0) int mEnd;
386 
387     private final @NonNull Params mParams;
388 
389     // The list of measured paragraph info.
390     private final @NonNull ParagraphInfo[] mParagraphInfo;
391 
392     /**
393      * Create a new {@link PrecomputedText} which will pre-compute text measurement and glyph
394      * positioning information.
395      * <p>
396      * This can be expensive, so computing this on a background thread before your text will be
397      * presented can save work on the UI thread.
398      * </p>
399      *
400      * Note that any {@link android.text.NoCopySpan} attached to the text won't be passed to the
401      * created PrecomputedText.
402      *
403      * @param text the text to be measured
404      * @param params parameters that define how text will be precomputed
405      * @return A {@link PrecomputedText}
406      */
create(@onNull CharSequence text, @NonNull Params params)407     public static PrecomputedText create(@NonNull CharSequence text, @NonNull Params params) {
408         ParagraphInfo[] paraInfo = null;
409         if (text instanceof PrecomputedText) {
410             final PrecomputedText hintPct = (PrecomputedText) text;
411             final PrecomputedText.Params hintParams = hintPct.getParams();
412             final @Params.CheckResultUsableResult int checkResult =
413                     hintParams.checkResultUsable(params.mPaint, params.mTextDir,
414                             params.mBreakStrategy, params.mHyphenationFrequency,
415                             params.mLineBreakConfig);
416             switch (checkResult) {
417                 case Params.USABLE:
418                     return hintPct;
419                 case Params.NEED_RECOMPUTE:
420                     // To be able to use PrecomputedText for new params, at least break strategy and
421                     // hyphenation frequency must be the same.
422                     if (params.getBreakStrategy() == hintParams.getBreakStrategy()
423                             && params.getHyphenationFrequency()
424                                 == hintParams.getHyphenationFrequency()) {
425                         paraInfo = createMeasuredParagraphsFromPrecomputedText(
426                                 hintPct, params, true /* compute layout */);
427                     }
428                     break;
429                 case Params.UNUSABLE:
430                     // Unable to use anything in PrecomputedText. Create PrecomputedText as the
431                     // normal text input.
432             }
433 
434         }
435         if (paraInfo == null) {
436             paraInfo = createMeasuredParagraphs(
437                     text, params, 0, text.length(), true /* computeLayout */,
438                     true /* computeBounds */);
439         }
440         return new PrecomputedText(text, 0, text.length(), params, paraInfo);
441     }
442 
isFastHyphenation(int frequency)443     private static boolean isFastHyphenation(int frequency) {
444         return frequency == Layout.HYPHENATION_FREQUENCY_FULL_FAST
445                 || frequency == Layout.HYPHENATION_FREQUENCY_NORMAL_FAST;
446     }
447 
createMeasuredParagraphsFromPrecomputedText( @onNull PrecomputedText pct, @NonNull Params params, boolean computeLayout)448     private static ParagraphInfo[] createMeasuredParagraphsFromPrecomputedText(
449             @NonNull PrecomputedText pct, @NonNull Params params, boolean computeLayout) {
450         final boolean needHyphenation = params.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE
451                 && params.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE;
452         final int hyphenationMode;
453         if (needHyphenation) {
454             hyphenationMode = isFastHyphenation(params.getHyphenationFrequency())
455                     ? MeasuredText.Builder.HYPHENATION_MODE_FAST :
456                     MeasuredText.Builder.HYPHENATION_MODE_NORMAL;
457         } else {
458             hyphenationMode = MeasuredText.Builder.HYPHENATION_MODE_NONE;
459         }
460         LineBreakConfig config = params.getLineBreakConfig();
461         if (config.getLineBreakWordStyle() == LineBreakConfig.LINE_BREAK_WORD_STYLE_AUTO
462                 && pct.getParagraphCount() != 1) {
463             // If the text has multiple paragraph, resolve line break word style auto to none.
464             config = new LineBreakConfig.Builder()
465                     .merge(config)
466                     .setLineBreakWordStyle(LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE)
467                     .build();
468         }
469         ArrayList<ParagraphInfo> result = new ArrayList<>();
470         for (int i = 0; i < pct.getParagraphCount(); ++i) {
471             final int paraStart = pct.getParagraphStart(i);
472             final int paraEnd = pct.getParagraphEnd(i);
473             result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout(
474                     params.getTextPaint(), config, pct, paraStart, paraEnd,
475                     params.getTextDirection(), hyphenationMode, computeLayout, true,
476                     pct.getMeasuredParagraph(i), null /* no recycle */)));
477         }
478         return result.toArray(new ParagraphInfo[result.size()]);
479     }
480 
481     /** @hide */
createMeasuredParagraphs( @onNull CharSequence text, @NonNull Params params, @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean computeLayout, boolean computeBounds)482     public static ParagraphInfo[] createMeasuredParagraphs(
483             @NonNull CharSequence text, @NonNull Params params,
484             @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean computeLayout,
485             boolean computeBounds) {
486         ArrayList<ParagraphInfo> result = new ArrayList<>();
487 
488         Preconditions.checkNotNull(text);
489         Preconditions.checkNotNull(params);
490         final boolean needHyphenation = params.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE
491                 && params.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE;
492         final int hyphenationMode;
493         if (needHyphenation) {
494             hyphenationMode = isFastHyphenation(params.getHyphenationFrequency())
495                     ? MeasuredText.Builder.HYPHENATION_MODE_FAST :
496                     MeasuredText.Builder.HYPHENATION_MODE_NORMAL;
497         } else {
498             hyphenationMode = MeasuredText.Builder.HYPHENATION_MODE_NONE;
499         }
500 
501         LineBreakConfig config = null;
502         int paraEnd = 0;
503         for (int paraStart = start; paraStart < end; paraStart = paraEnd) {
504             paraEnd = TextUtils.indexOf(text, LINE_FEED, paraStart, end);
505             if (paraEnd < 0) {
506                 // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph
507                 // end.
508                 paraEnd = end;
509             } else {
510                 paraEnd++;  // Includes LINE_FEED(U+000A) to the prev paragraph.
511             }
512 
513             if (config == null) {
514                 config = params.getLineBreakConfig();
515                 if (config.getLineBreakWordStyle() == LineBreakConfig.LINE_BREAK_WORD_STYLE_AUTO
516                         && !(paraStart == start && paraEnd == end)) {
517                     // If the text has multiple paragraph, resolve line break word style auto to
518                     // none.
519                     config = new LineBreakConfig.Builder()
520                             .merge(config)
521                             .setLineBreakWordStyle(LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE)
522                             .build();
523                 }
524             }
525 
526             result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout(
527                     params.getTextPaint(), config, text, paraStart, paraEnd,
528                     params.getTextDirection(), hyphenationMode, computeLayout, computeBounds,
529                     null /* no hint */,
530                     null /* no recycle */)));
531         }
532         return result.toArray(new ParagraphInfo[result.size()]);
533     }
534 
535     // Use PrecomputedText.create instead.
PrecomputedText(@onNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull Params params, @NonNull ParagraphInfo[] paraInfo)536     private PrecomputedText(@NonNull CharSequence text, @IntRange(from = 0) int start,
537             @IntRange(from = 0) int end, @NonNull Params params,
538             @NonNull ParagraphInfo[] paraInfo) {
539         mText = new SpannableString(text, true /* ignoreNoCopySpan */);
540         mStart = start;
541         mEnd = end;
542         mParams = params;
543         mParagraphInfo = paraInfo;
544     }
545 
546     /**
547      * Return the underlying text.
548      * @hide
549      */
getText()550     public @NonNull CharSequence getText() {
551         return mText;
552     }
553 
554     /**
555      * Returns the inclusive start offset of measured region.
556      * @hide
557      */
getStart()558     public @IntRange(from = 0) int getStart() {
559         return mStart;
560     }
561 
562     /**
563      * Returns the exclusive end offset of measured region.
564      * @hide
565      */
getEnd()566     public @IntRange(from = 0) int getEnd() {
567         return mEnd;
568     }
569 
570     /**
571      * Returns the layout parameters used to measure this text.
572      */
getParams()573     public @NonNull Params getParams() {
574         return mParams;
575     }
576 
577     /**
578      * Returns the count of paragraphs.
579      */
getParagraphCount()580     public @IntRange(from = 0) int getParagraphCount() {
581         return mParagraphInfo.length;
582     }
583 
584     /**
585      * Returns the paragraph start offset of the text.
586      */
getParagraphStart(@ntRangefrom = 0) int paraIndex)587     public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) {
588         Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
589         return paraIndex == 0 ? mStart : getParagraphEnd(paraIndex - 1);
590     }
591 
592     /**
593      * Returns the paragraph end offset of the text.
594      */
getParagraphEnd(@ntRangefrom = 0) int paraIndex)595     public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) {
596         Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
597         return mParagraphInfo[paraIndex].paragraphEnd;
598     }
599 
600     /** @hide */
getMeasuredParagraph(@ntRangefrom = 0) int paraIndex)601     public @NonNull MeasuredParagraph getMeasuredParagraph(@IntRange(from = 0) int paraIndex) {
602         return mParagraphInfo[paraIndex].measured;
603     }
604 
605     /** @hide */
getParagraphInfo()606     public @NonNull ParagraphInfo[] getParagraphInfo() {
607         return mParagraphInfo;
608     }
609 
610     /**
611      * Returns true if the given TextPaint gives the same result of text layout for this text.
612      * @hide
613      */
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)614     public @Params.CheckResultUsableResult int checkResultUsable(@IntRange(from = 0) int start,
615             @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir,
616             @NonNull TextPaint paint, @Layout.BreakStrategy int strategy,
617             @Layout.HyphenationFrequency int frequency, @NonNull LineBreakConfig lbConfig) {
618         if (mStart != start || mEnd != end) {
619             return Params.UNUSABLE;
620         } else {
621             return mParams.checkResultUsable(paint, textDir, strategy, frequency, lbConfig);
622         }
623     }
624 
625     /** @hide */
findParaIndex(@ntRangefrom = 0) int pos)626     public int findParaIndex(@IntRange(from = 0) int pos) {
627         // TODO: Maybe good to remove paragraph concept from PrecomputedText and add substring
628         //       layout support to StaticLayout.
629         for (int i = 0; i < mParagraphInfo.length; ++i) {
630             if (pos < mParagraphInfo[i].paragraphEnd) {
631                 return i;
632             }
633         }
634         throw new IndexOutOfBoundsException(
635             "pos must be less than " + mParagraphInfo[mParagraphInfo.length - 1].paragraphEnd
636             + ", gave " + pos);
637     }
638 
639     /**
640      * Returns text width for the given range.
641      * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise
642      * IllegalArgumentException will be thrown.
643      *
644      * @param start the inclusive start offset in the text
645      * @param end the exclusive end offset in the text
646      * @return the text width
647      * @throws IllegalArgumentException if start and end offset are in the different paragraph.
648      */
getWidth(@ntRangefrom = 0) int start, @IntRange(from = 0) int end)649     public @FloatRange(from = 0) float getWidth(@IntRange(from = 0) int start,
650             @IntRange(from = 0) int end) {
651         Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset");
652         Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset");
653         Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset");
654 
655         if (start == end) {
656             return 0;
657         }
658         final int paraIndex = findParaIndex(start);
659         final int paraStart = getParagraphStart(paraIndex);
660         final int paraEnd = getParagraphEnd(paraIndex);
661         if (start < paraStart || paraEnd < end) {
662             throw new IllegalArgumentException("Cannot measured across the paragraph:"
663                 + "para: (" + paraStart + ", " + paraEnd + "), "
664                 + "request: (" + start + ", " + end + ")");
665         }
666         return getMeasuredParagraph(paraIndex).getWidth(start - paraStart, end - paraStart);
667     }
668 
669     /**
670      * Retrieves the text bounding box for the given range.
671      * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise
672      * IllegalArgumentException will be thrown.
673      *
674      * @param start the inclusive start offset in the text
675      * @param end the exclusive end offset in the text
676      * @param bounds the output rectangle
677      * @throws IllegalArgumentException if start and end offset are in the different paragraph.
678      */
getBounds(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull Rect bounds)679     public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
680             @NonNull Rect bounds) {
681         Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset");
682         Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset");
683         Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset");
684         Preconditions.checkNotNull(bounds);
685         if (start == end) {
686             bounds.set(0, 0, 0, 0);
687             return;
688         }
689         final int paraIndex = findParaIndex(start);
690         final int paraStart = getParagraphStart(paraIndex);
691         final int paraEnd = getParagraphEnd(paraIndex);
692         if (start < paraStart || paraEnd < end) {
693             throw new IllegalArgumentException("Cannot measured across the paragraph:"
694                 + "para: (" + paraStart + ", " + paraEnd + "), "
695                 + "request: (" + start + ", " + end + ")");
696         }
697         getMeasuredParagraph(paraIndex).getBounds(start - paraStart, end - paraStart, bounds);
698     }
699 
700     /**
701      * Retrieves the text font metrics for the given range.
702      * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise
703      * IllegalArgumentException will be thrown.
704      *
705      * @param start the inclusive start offset in the text
706      * @param end the exclusive end offset in the text
707      * @param outMetrics the output font metrics
708      * @throws IllegalArgumentException if start and end offset are in the different paragraph.
709      */
getFontMetricsInt(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull Paint.FontMetricsInt outMetrics)710     public void getFontMetricsInt(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
711             @NonNull Paint.FontMetricsInt outMetrics) {
712         Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset");
713         Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset");
714         Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset");
715         Objects.requireNonNull(outMetrics);
716         if (start == end) {
717             mParams.getTextPaint().getFontMetricsInt(outMetrics);
718             return;
719         }
720         final int paraIndex = findParaIndex(start);
721         final int paraStart = getParagraphStart(paraIndex);
722         final int paraEnd = getParagraphEnd(paraIndex);
723         if (start < paraStart || paraEnd < end) {
724             throw new IllegalArgumentException("Cannot measured across the paragraph:"
725                     + "para: (" + paraStart + ", " + paraEnd + "), "
726                     + "request: (" + start + ", " + end + ")");
727         }
728         getMeasuredParagraph(paraIndex).getFontMetricsInt(start - paraStart,
729                 end - paraStart, outMetrics);
730     }
731 
732     /**
733      * Returns a width of a character at offset
734      *
735      * @param offset an offset of the text.
736      * @return a width of the character.
737      * @hide
738      */
getCharWidthAt(@ntRangefrom = 0) int offset)739     public float getCharWidthAt(@IntRange(from = 0) int offset) {
740         Preconditions.checkArgument(0 <= offset && offset < mText.length(), "invalid offset");
741         final int paraIndex = findParaIndex(offset);
742         final int paraStart = getParagraphStart(paraIndex);
743         final int paraEnd = getParagraphEnd(paraIndex);
744         return getMeasuredParagraph(paraIndex).getCharWidthAt(offset - paraStart);
745     }
746 
747     /**
748      * Returns the size of native PrecomputedText memory usage.
749      *
750      * Note that this is not guaranteed to be accurate. Must be used only for testing purposes.
751      * @hide
752      */
getMemoryUsage()753     public int getMemoryUsage() {
754         int r = 0;
755         for (int i = 0; i < getParagraphCount(); ++i) {
756             r += getMeasuredParagraph(i).getMemoryUsage();
757         }
758         return r;
759     }
760 
761     ///////////////////////////////////////////////////////////////////////////////////////////////
762     // Spannable overrides
763     //
764     // Do not allow to modify MetricAffectingSpan
765 
766     /**
767      * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified.
768      */
769     @Override
setSpan(Object what, int start, int end, int flags)770     public void setSpan(Object what, int start, int end, int flags) {
771         if (what instanceof MetricAffectingSpan) {
772             throw new IllegalArgumentException(
773                     "MetricAffectingSpan can not be set to PrecomputedText.");
774         }
775         mText.setSpan(what, start, end, flags);
776     }
777 
778     /**
779      * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified.
780      */
781     @Override
removeSpan(Object what)782     public void removeSpan(Object what) {
783         if (what instanceof MetricAffectingSpan) {
784             throw new IllegalArgumentException(
785                     "MetricAffectingSpan can not be removed from PrecomputedText.");
786         }
787         mText.removeSpan(what);
788     }
789 
790     ///////////////////////////////////////////////////////////////////////////////////////////////
791     // Spanned overrides
792     //
793     // Just proxy for underlying mText if appropriate.
794 
795     @Override
getSpans(int start, int end, Class<T> type)796     public <T> T[] getSpans(int start, int end, Class<T> type) {
797         return mText.getSpans(start, end, type);
798     }
799 
800     @Override
getSpanStart(Object tag)801     public int getSpanStart(Object tag) {
802         return mText.getSpanStart(tag);
803     }
804 
805     @Override
getSpanEnd(Object tag)806     public int getSpanEnd(Object tag) {
807         return mText.getSpanEnd(tag);
808     }
809 
810     @Override
getSpanFlags(Object tag)811     public int getSpanFlags(Object tag) {
812         return mText.getSpanFlags(tag);
813     }
814 
815     @Override
nextSpanTransition(int start, int limit, Class type)816     public int nextSpanTransition(int start, int limit, Class type) {
817         return mText.nextSpanTransition(start, limit, type);
818     }
819 
820     ///////////////////////////////////////////////////////////////////////////////////////////////
821     // CharSequence overrides.
822     //
823     // Just proxy for underlying mText.
824 
825     @Override
length()826     public int length() {
827         return mText.length();
828     }
829 
830     @Override
charAt(int index)831     public char charAt(int index) {
832         return mText.charAt(index);
833     }
834 
835     @Override
subSequence(int start, int end)836     public CharSequence subSequence(int start, int end) {
837         return PrecomputedText.create(mText.subSequence(start, end), mParams);
838     }
839 
840     @Override
toString()841     public String toString() {
842         return mText.toString();
843     }
844 }
845