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