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