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