diff options
Diffstat (limited to 'core/java/android')
| -rw-r--r-- | core/java/android/text/BoringLayout.java | 4 | ||||
| -rw-r--r-- | core/java/android/text/MeasuredText.java | 427 | ||||
| -rw-r--r-- | core/java/android/text/PrecomputedText.java | 501 | ||||
| -rw-r--r-- | core/java/android/text/StaticLayout.java | 39 | ||||
| -rw-r--r-- | core/java/android/text/TextLine.java | 17 | ||||
| -rw-r--r-- | core/java/android/view/RecordingCanvas.java | 8 | ||||
| -rw-r--r-- | core/java/android/widget/TextView.java | 36 |
7 files changed, 559 insertions, 473 deletions
diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java index 6fa5312be5cc..ae59bfaf896f 100644 --- a/core/java/android/text/BoringLayout.java +++ b/core/java/android/text/BoringLayout.java @@ -347,8 +347,8 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback TextLine line = TextLine.obtain(); line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT, Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null); - if (text instanceof MeasuredText) { - MeasuredText mt = (MeasuredText) text; + if (text instanceof PrecomputedText) { + PrecomputedText mt = (PrecomputedText) text; // Reaching here means there is only one paragraph. MeasuredParagraph mp = mt.getMeasuredParagraph(0); fm.width = (int) Math.ceil(mp.getWidth(0, mp.getTextLength())); diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java deleted file mode 100644 index bb7a9e0b7906..000000000000 --- a/core/java/android/text/MeasuredText.java +++ /dev/null @@ -1,427 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.text; - -import android.annotation.IntRange; -import android.annotation.NonNull; -import android.util.IntArray; - -import com.android.internal.util.ArrayUtils; -import com.android.internal.util.Preconditions; - -import java.util.ArrayList; - -/** - * A text which has already been measured. - */ -public class MeasuredText implements Spanned { - private static final char LINE_FEED = '\n'; - - // The original text. - private final @NonNull CharSequence mText; - - // The inclusive start offset of the measuring target. - private final @IntRange(from = 0) int mStart; - - // The exclusive end offset of the measuring target. - private final @IntRange(from = 0) int mEnd; - - // The TextPaint used for measurement. - private final @NonNull TextPaint mPaint; - - // The requested text direction. - private final @NonNull TextDirectionHeuristic mTextDir; - - // The measured paragraph texts. - private final @NonNull MeasuredParagraph[] mMeasuredParagraphs; - - // The sorted paragraph end offsets. - private final @NonNull int[] mParagraphBreakPoints; - - // The break strategy for this measured text. - private final @Layout.BreakStrategy int mBreakStrategy; - - // The hyphenation frequency for this measured text. - private final @Layout.HyphenationFrequency int mHyphenationFrequency; - - /** - * A Builder for MeasuredText - */ - public static final class Builder { - // Mandatory parameters. - private final @NonNull CharSequence mText; - private final @NonNull TextPaint mPaint; - - // Members to be updated by setters. - private @IntRange(from = 0) int mStart; - private @IntRange(from = 0) int mEnd; - private TextDirectionHeuristic mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR; - private @Layout.BreakStrategy int mBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY; - private @Layout.HyphenationFrequency int mHyphenationFrequency = - Layout.HYPHENATION_FREQUENCY_NORMAL; - - - /** - * Builder constructor - * - * @param text The text to be measured. - * @param paint The paint to be used for drawing. - */ - public Builder(@NonNull CharSequence text, @NonNull TextPaint paint) { - Preconditions.checkNotNull(text); - Preconditions.checkNotNull(paint); - - mText = text; - mPaint = paint; - mStart = 0; - mEnd = text.length(); - } - - /** - * Set the range of measuring target. - * - * @param start The measuring target start offset in the text. - * @param end The measuring target end offset in the text. - */ - public @NonNull Builder setRange(@IntRange(from = 0) int start, - @IntRange(from = 0) int end) { - Preconditions.checkArgumentInRange(start, 0, mText.length(), "start"); - Preconditions.checkArgumentInRange(end, 0, mText.length(), "end"); - Preconditions.checkArgument(start <= end, "The range is reversed."); - - mStart = start; - mEnd = end; - return this; - } - - /** - * Set the text direction heuristic - * - * The default value is {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}. - * - * @param textDir The text direction heuristic for resolving bidi behavior. - * @return this builder, useful for chaining. - */ - public @NonNull Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) { - Preconditions.checkNotNull(textDir); - mTextDir = textDir; - return this; - } - - /** - * Set the break strategy - * - * The default value is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}. - * - * @param breakStrategy The break strategy. - * @return this builder, useful for chaining. - */ - public @NonNull Builder setBreakStrategy(@Layout.BreakStrategy int breakStrategy) { - mBreakStrategy = breakStrategy; - return this; - } - - /** - * Set the hyphenation frequency - * - * The default value is {@link Layout#HYPHENATION_FREQUENCY_NORMAL}. - * - * @param hyphenationFrequency The hyphenation frequency. - * @return this builder, useful for chaining. - */ - public @NonNull Builder setHyphenationFrequency( - @Layout.HyphenationFrequency int hyphenationFrequency) { - mHyphenationFrequency = hyphenationFrequency; - return this; - } - - /** - * Build the measured text - * - * @return the measured text. - */ - public @NonNull MeasuredText build() { - return build(true /* build full layout result */); - } - - /** @hide */ - public @NonNull MeasuredText build(boolean computeLayout) { - final boolean needHyphenation = mBreakStrategy != Layout.BREAK_STRATEGY_SIMPLE - && mHyphenationFrequency != Layout.HYPHENATION_FREQUENCY_NONE; - - final IntArray paragraphEnds = new IntArray(); - final ArrayList<MeasuredParagraph> measuredTexts = new ArrayList<>(); - - int paraEnd = 0; - for (int paraStart = mStart; paraStart < mEnd; paraStart = paraEnd) { - paraEnd = TextUtils.indexOf(mText, LINE_FEED, paraStart, mEnd); - if (paraEnd < 0) { - // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph - // end. - paraEnd = mEnd; - } else { - paraEnd++; // Includes LINE_FEED(U+000A) to the prev paragraph. - } - - paragraphEnds.add(paraEnd); - measuredTexts.add(MeasuredParagraph.buildForStaticLayout( - mPaint, mText, paraStart, paraEnd, mTextDir, needHyphenation, - computeLayout, null /* no recycle */)); - } - - return new MeasuredText(mText, mStart, mEnd, mPaint, mTextDir, mBreakStrategy, - mHyphenationFrequency, measuredTexts.toArray( - new MeasuredParagraph[measuredTexts.size()]), - paragraphEnds.toArray()); - } - }; - - // Use MeasuredText.Builder instead. - private MeasuredText(@NonNull CharSequence text, - @IntRange(from = 0) int start, - @IntRange(from = 0) int end, - @NonNull TextPaint paint, - @NonNull TextDirectionHeuristic textDir, - @Layout.BreakStrategy int breakStrategy, - @Layout.HyphenationFrequency int frequency, - @NonNull MeasuredParagraph[] measuredTexts, - @NonNull int[] paragraphBreakPoints) { - mText = text; - mStart = start; - mEnd = end; - // Copy the paint so that we can keep the reference of typeface in native layout result. - mPaint = new TextPaint(paint); - mMeasuredParagraphs = measuredTexts; - mParagraphBreakPoints = paragraphBreakPoints; - mTextDir = textDir; - mBreakStrategy = breakStrategy; - mHyphenationFrequency = frequency; - } - - /** - * Return the underlying text. - */ - public @NonNull CharSequence getText() { - return mText; - } - - /** - * Returns the inclusive start offset of measured region. - */ - public @IntRange(from = 0) int getStart() { - return mStart; - } - - /** - * Returns the exclusive end offset of measured region. - */ - public @IntRange(from = 0) int getEnd() { - return mEnd; - } - - /** - * Returns the text direction associated with char sequence. - */ - public @NonNull TextDirectionHeuristic getTextDir() { - return mTextDir; - } - - /** - * Returns the paint used to measure this text. - */ - public @NonNull TextPaint getPaint() { - return mPaint; - } - - /** - * Returns the length of the paragraph of this text. - */ - public @IntRange(from = 0) int getParagraphCount() { - return mParagraphBreakPoints.length; - } - - /** - * Returns the paragraph start offset of the text. - */ - public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) { - Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex"); - return paraIndex == 0 ? mStart : mParagraphBreakPoints[paraIndex - 1]; - } - - /** - * Returns the paragraph end offset of the text. - */ - public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) { - Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex"); - return mParagraphBreakPoints[paraIndex]; - } - - /** @hide */ - public @NonNull MeasuredParagraph getMeasuredParagraph(@IntRange(from = 0) int paraIndex) { - return mMeasuredParagraphs[paraIndex]; - } - - /** - * Returns the break strategy for this text. - */ - public @Layout.BreakStrategy int getBreakStrategy() { - return mBreakStrategy; - } - - /** - * Returns the hyphenation frequency for this text. - */ - public @Layout.HyphenationFrequency int getHyphenationFrequency() { - return mHyphenationFrequency; - } - - /** - * Returns true if the given TextPaint gives the same result of text layout for this text. - * @hide - */ - public boolean canUseMeasuredResult(@NonNull TextPaint paint) { - return mPaint.getTextSize() == paint.getTextSize() - && mPaint.getTextSkewX() == paint.getTextSkewX() - && mPaint.getTextScaleX() == paint.getTextScaleX() - && mPaint.getLetterSpacing() == paint.getLetterSpacing() - && mPaint.getWordSpacing() == paint.getWordSpacing() - && mPaint.getFlags() == paint.getFlags() // Maybe not all flag affects text layout. - && mPaint.getTextLocales() == paint.getTextLocales() // need to be equals? - && mPaint.getFontVariationSettings() == paint.getFontVariationSettings() - && mPaint.getTypeface() == paint.getTypeface() - && TextUtils.equals(mPaint.getFontFeatureSettings(), paint.getFontFeatureSettings()); - } - - /** @hide */ - public int findParaIndex(@IntRange(from = 0) int pos) { - // TODO: Maybe good to remove paragraph concept from MeasuredText and add substring layout - // support to StaticLayout. - for (int i = 0; i < mParagraphBreakPoints.length; ++i) { - if (pos < mParagraphBreakPoints[i]) { - return i; - } - } - throw new IndexOutOfBoundsException( - "pos must be less than " + mParagraphBreakPoints[mParagraphBreakPoints.length - 1] - + ", gave " + pos); - } - - /** @hide */ - public float getWidth(@IntRange(from = 0) int start, @IntRange(from = 0) int end) { - final int paraIndex = findParaIndex(start); - final int paraStart = getParagraphStart(paraIndex); - final int paraEnd = getParagraphEnd(paraIndex); - if (start < paraStart || paraEnd < end) { - throw new RuntimeException("Cannot measured across the paragraph:" - + "para: (" + paraStart + ", " + paraEnd + "), " - + "request: (" + start + ", " + end + ")"); - } - return getMeasuredParagraph(paraIndex).getWidth(start - paraStart, end - paraStart); - } - - /** - * Returns the size of native MeasuredText memory usage - * - * Note that this may not be aculate. Must be used only for testing purposes. - * @hide - */ - public int getMemoryUsage() { - int r = 0; - for (int i = 0; i < getParagraphCount(); ++i) { - r += getMeasuredParagraph(i).getMemoryUsage(); - } - return r; - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // Spanned overrides - // - // Just proxy for underlying mText if appropriate. - - @Override - public <T> T[] getSpans(int start, int end, Class<T> type) { - if (mText instanceof Spanned) { - return ((Spanned) mText).getSpans(start, end, type); - } else { - return ArrayUtils.emptyArray(type); - } - } - - @Override - public int getSpanStart(Object tag) { - if (mText instanceof Spanned) { - return ((Spanned) mText).getSpanStart(tag); - } else { - return -1; - } - } - - @Override - public int getSpanEnd(Object tag) { - if (mText instanceof Spanned) { - return ((Spanned) mText).getSpanEnd(tag); - } else { - return -1; - } - } - - @Override - public int getSpanFlags(Object tag) { - if (mText instanceof Spanned) { - return ((Spanned) mText).getSpanFlags(tag); - } else { - return 0; - } - } - - @Override - public int nextSpanTransition(int start, int limit, Class type) { - if (mText instanceof Spanned) { - return ((Spanned) mText).nextSpanTransition(start, limit, type); - } else { - return mText.length(); - } - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // CharSequence overrides. - // - // Just proxy for underlying mText. - - @Override - public int length() { - return mText.length(); - } - - @Override - public char charAt(int index) { - // TODO: Should this be index + mStart ? - return mText.charAt(index); - } - - @Override - public CharSequence subSequence(int start, int end) { - // TODO: return MeasuredText. - // TODO: Should this be index + mStart, end + mStart ? - return mText.subSequence(start, end); - } - - @Override - public String toString() { - return mText.toString(); - } -} diff --git a/core/java/android/text/PrecomputedText.java b/core/java/android/text/PrecomputedText.java new file mode 100644 index 000000000000..c211e2dba3c0 --- /dev/null +++ b/core/java/android/text/PrecomputedText.java @@ -0,0 +1,501 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.IntArray; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.Objects; + +/** + * A text which has the character metrics data. + * + * A text object that contains the character metrics data and can be used to improve the performance + * of text layout operations. When a PrecomputedText is created with a given {@link CharSequence}, + * it will measure the text metrics during the creation. This PrecomputedText instance can be set on + * {@link android.widget.TextView} or {@link StaticLayout}. Since the text layout information will + * be included in this instance, {@link android.widget.TextView} or {@link StaticLayout} will not + * have to recalculate this information. + * + * Note that the {@link PrecomputedText} created from different parameters of the target {@link + * android.widget.TextView} will be rejected internally and compute the text layout again with the + * current {@link android.widget.TextView} parameters. + * + * <pre> + * An example usage is: + * <code> + * void asyncSetText(final TextView textView, final String longString, Handler bgThreadHandler) { + * // construct precompute related parameters using the TextView that we will set the text on. + * final PrecomputedText.Params params = textView.getTextParams(); + * bgThreadHandler.post(() -> { + * final PrecomputedText precomputedText = + * PrecomputedText.create(expensiveLongString, params); + * textView.post(() -> { + * textView.setText(precomputedText); + * }); + * }); + * } + * </code> + * </pre> + * + * Note that the {@link PrecomputedText} created from different parameters of the target + * {@link android.widget.TextView} will be rejected internally and compute the text layout again + * with the current {@link android.widget.TextView} parameters. + */ +public class PrecomputedText implements Spanned { + private static final char LINE_FEED = '\n'; + + /** + * The information required for building {@link PrecomputedText}. + * + * Contains information required for precomputing text measurement metadata, so it can be done + * in isolation of a {@link android.widget.TextView} or {@link StaticLayout}, when final layout + * constraints are not known. + */ + public static final class Params { + // The TextPaint used for measurement. + private final @NonNull TextPaint mPaint; + + // The requested text direction. + private final @NonNull TextDirectionHeuristic mTextDir; + + // The break strategy for this measured text. + private final @Layout.BreakStrategy int mBreakStrategy; + + // The hyphenation frequency for this measured text. + private final @Layout.HyphenationFrequency int mHyphenationFrequency; + + /** + * A builder for creating {@link Params}. + */ + public static class Builder { + // The TextPaint used for measurement. + private final @NonNull TextPaint mPaint; + + // The requested text direction. + private TextDirectionHeuristic mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR; + + // The break strategy for this measured text. + private @Layout.BreakStrategy int mBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY; + + // The hyphenation frequency for this measured text. + private @Layout.HyphenationFrequency int mHyphenationFrequency = + Layout.HYPHENATION_FREQUENCY_NORMAL; + + /** + * Builder constructor. + * + * @param paint the paint to be used for drawing + */ + public Builder(@NonNull TextPaint paint) { + mPaint = paint; + } + + /** + * Set the line break strategy. + * + * The default value is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}. + * + * @param strategy the break strategy + * @return this builder, useful for chaining + * @see StaticLayout.Builder#setBreakStrategy + * @see android.widget.TextView#setBreakStrategy + */ + public Builder setBreakStrategy(@Layout.BreakStrategy int strategy) { + mBreakStrategy = strategy; + return this; + } + + /** + * Set the hyphenation frequency. + * + * The default value is {@link Layout#HYPHENATION_FREQUENCY_NORMAL}. + * + * @param frequency the hyphenation frequency + * @return this builder, useful for chaining + * @see StaticLayout.Builder#setHyphenationFrequency + * @see android.widget.TextView#setHyphenationFrequency + */ + public Builder setHyphenationFrequency(@Layout.HyphenationFrequency int frequency) { + mHyphenationFrequency = frequency; + return this; + } + + /** + * Set the text direction heuristic. + * + * The default value is {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}. + * + * @param textDir the text direction heuristic for resolving bidi behavior + * @return this builder, useful for chaining + * @see StaticLayout.Builder#setTextDirection + */ + public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) { + mTextDir = textDir; + return this; + } + + /** + * Build the {@link Params}. + * + * @return the layout parameter + */ + public @NonNull Params build() { + return new Params(mPaint, mTextDir, mBreakStrategy, mHyphenationFrequency); + } + } + + // This is public hidden for internal use. + // For the external developers, use Builder instead. + /** @hide */ + public Params(@NonNull TextPaint paint, @NonNull TextDirectionHeuristic textDir, + @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency) { + mPaint = paint; + mTextDir = textDir; + mBreakStrategy = strategy; + mHyphenationFrequency = frequency; + } + + /** + * Returns the {@link TextPaint} for this text. + * + * @return A {@link TextPaint} + */ + public @NonNull TextPaint getTextPaint() { + return mPaint; + } + + /** + * Returns the {@link TextDirectionHeuristic} for this text. + * + * @return A {@link TextDirectionHeuristic} + */ + public @NonNull TextDirectionHeuristic getTextDirection() { + return mTextDir; + } + + /** + * Returns the break strategy for this text. + * + * @return A line break strategy + */ + public @Layout.BreakStrategy int getBreakStrategy() { + return mBreakStrategy; + } + + /** + * Returns the hyphenation frequency for this text. + * + * @return A hyphenation frequency + */ + public @Layout.HyphenationFrequency int getHyphenationFrequency() { + return mHyphenationFrequency; + } + + private boolean isSameTextMetricsInternal(@NonNull TextPaint paint, + @NonNull TextDirectionHeuristic textDir, @Layout.BreakStrategy int strategy, + @Layout.HyphenationFrequency int frequency) { + return mTextDir == textDir + && mBreakStrategy == strategy + && mHyphenationFrequency == frequency + && mPaint.equalsForTextMeasurement(paint); + } + + /** + * Check if the same text layout. + * + * @return true if this and the given param result in the same text layout + */ + @Override + public boolean equals(@Nullable Object o) { + if (o == this) { + return true; + } + if (o == null || !(o instanceof Params)) { + return false; + } + Params param = (Params) o; + return isSameTextMetricsInternal(param.mPaint, param.mTextDir, param.mBreakStrategy, + param.mHyphenationFrequency); + } + + @Override + public int hashCode() { + // TODO: implement MinikinPaint::hashCode and use it to keep consistency with equals. + return Objects.hash(mPaint.getTextSize(), mPaint.getTextScaleX(), mPaint.getTextSkewX(), + mPaint.getLetterSpacing(), mPaint.getWordSpacing(), mPaint.getFlags(), + mPaint.getTextLocales(), mPaint.getTypeface(), + mPaint.getFontVariationSettings(), mPaint.isElegantTextHeight(), mTextDir, + mBreakStrategy, mHyphenationFrequency); + } + }; + + // The original text. + private final @NonNull SpannedString mText; + + // The inclusive start offset of the measuring target. + private final @IntRange(from = 0) int mStart; + + // The exclusive end offset of the measuring target. + private final @IntRange(from = 0) int mEnd; + + private final @NonNull Params mParams; + + // The measured paragraph texts. + private final @NonNull MeasuredParagraph[] mMeasuredParagraphs; + + // The sorted paragraph end offsets. + private final @NonNull int[] mParagraphBreakPoints; + + /** + * Create a new {@link PrecomputedText} which will pre-compute text measurement and glyph + * positioning information. + * <p> + * This can be expensive, so computing this on a background thread before your text will be + * presented can save work on the UI thread. + * </p> + * + * @param text the text to be measured + * @param param parameters that define how text will be precomputed + * @return A {@link PrecomputedText} + */ + public static PrecomputedText create(@NonNull CharSequence text, @NonNull Params param) { + return createInternal(text, param, 0, text.length(), true /* compute full Layout */); + } + + /** @hide */ + public static PrecomputedText createWidthOnly(@NonNull CharSequence text, @NonNull Params param, + @IntRange(from = 0) int start, @IntRange(from = 0) int end) { + return createInternal(text, param, start, end, false /* compute width only */); + } + + private static PrecomputedText createInternal(@NonNull CharSequence text, @NonNull Params param, + @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean computeLayout) { + Preconditions.checkNotNull(text); + Preconditions.checkNotNull(param); + final boolean needHyphenation = param.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE + && param.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE; + + final IntArray paragraphEnds = new IntArray(); + final ArrayList<MeasuredParagraph> measuredTexts = new ArrayList<>(); + + int paraEnd = 0; + for (int paraStart = start; paraStart < end; paraStart = paraEnd) { + paraEnd = TextUtils.indexOf(text, LINE_FEED, paraStart, end); + if (paraEnd < 0) { + // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph + // end. + paraEnd = end; + } else { + paraEnd++; // Includes LINE_FEED(U+000A) to the prev paragraph. + } + + paragraphEnds.add(paraEnd); + measuredTexts.add(MeasuredParagraph.buildForStaticLayout( + param.getTextPaint(), text, paraStart, paraEnd, param.getTextDirection(), + needHyphenation, computeLayout, null /* no recycle */)); + } + + return new PrecomputedText(text, start, end, param, + measuredTexts.toArray(new MeasuredParagraph[measuredTexts.size()]), + paragraphEnds.toArray()); + } + + // Use PrecomputedText.create instead. + private PrecomputedText(@NonNull CharSequence text, @IntRange(from = 0) int start, + @IntRange(from = 0) int end, @NonNull Params param, + @NonNull MeasuredParagraph[] measuredTexts, @NonNull int[] paragraphBreakPoints) { + mText = new SpannedString(text); + mStart = start; + mEnd = end; + mParams = param; + mMeasuredParagraphs = measuredTexts; + mParagraphBreakPoints = paragraphBreakPoints; + } + + /** + * Return the underlying text. + */ + public @NonNull CharSequence getText() { + return mText; + } + + /** + * Returns the inclusive start offset of measured region. + * @hide + */ + public @IntRange(from = 0) int getStart() { + return mStart; + } + + /** + * Returns the exclusive end offset of measured region. + * @hide + */ + public @IntRange(from = 0) int getEnd() { + return mEnd; + } + + /** + * Returns the layout parameters used to measure this text. + */ + public @NonNull Params getParams() { + return mParams; + } + + /** + * Returns the count of paragraphs. + */ + public @IntRange(from = 0) int getParagraphCount() { + return mParagraphBreakPoints.length; + } + + /** + * Returns the paragraph start offset of the text. + */ + public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) { + Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex"); + return paraIndex == 0 ? mStart : mParagraphBreakPoints[paraIndex - 1]; + } + + /** + * Returns the paragraph end offset of the text. + */ + public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) { + Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex"); + return mParagraphBreakPoints[paraIndex]; + } + + /** @hide */ + public @NonNull MeasuredParagraph getMeasuredParagraph(@IntRange(from = 0) int paraIndex) { + return mMeasuredParagraphs[paraIndex]; + } + + /** + * Returns true if the given TextPaint gives the same result of text layout for this text. + * @hide + */ + public boolean canUseMeasuredResult(@IntRange(from = 0) int start, @IntRange(from = 0) int end, + @NonNull TextDirectionHeuristic textDir, @NonNull TextPaint paint, + @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency) { + final TextPaint mtPaint = mParams.getTextPaint(); + return mStart == start + && mEnd == end + && mParams.isSameTextMetricsInternal(paint, textDir, strategy, frequency); + } + + /** @hide */ + public int findParaIndex(@IntRange(from = 0) int pos) { + // TODO: Maybe good to remove paragraph concept from PrecomputedText and add substring + // layout support to StaticLayout. + for (int i = 0; i < mParagraphBreakPoints.length; ++i) { + if (pos < mParagraphBreakPoints[i]) { + return i; + } + } + throw new IndexOutOfBoundsException( + "pos must be less than " + mParagraphBreakPoints[mParagraphBreakPoints.length - 1] + + ", gave " + pos); + } + + /** @hide */ + public float getWidth(@IntRange(from = 0) int start, @IntRange(from = 0) int end) { + final int paraIndex = findParaIndex(start); + final int paraStart = getParagraphStart(paraIndex); + final int paraEnd = getParagraphEnd(paraIndex); + if (start < paraStart || paraEnd < end) { + throw new RuntimeException("Cannot measured across the paragraph:" + + "para: (" + paraStart + ", " + paraEnd + "), " + + "request: (" + start + ", " + end + ")"); + } + return getMeasuredParagraph(paraIndex).getWidth(start - paraStart, end - paraStart); + } + + /** + * Returns the size of native PrecomputedText memory usage. + * + * Note that this is not guaranteed to be accurate. Must be used only for testing purposes. + * @hide + */ + public int getMemoryUsage() { + int r = 0; + for (int i = 0; i < getParagraphCount(); ++i) { + r += getMeasuredParagraph(i).getMemoryUsage(); + } + return r; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // Spanned overrides + // + // Just proxy for underlying mText if appropriate. + + @Override + public <T> T[] getSpans(int start, int end, Class<T> type) { + return mText.getSpans(start, end, type); + } + + @Override + public int getSpanStart(Object tag) { + return mText.getSpanStart(tag); + } + + @Override + public int getSpanEnd(Object tag) { + return mText.getSpanEnd(tag); + } + + @Override + public int getSpanFlags(Object tag) { + return mText.getSpanFlags(tag); + } + + @Override + public int nextSpanTransition(int start, int limit, Class type) { + return mText.nextSpanTransition(start, limit, type); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CharSequence overrides. + // + // Just proxy for underlying mText. + + @Override + public int length() { + return mText.length(); + } + + @Override + public char charAt(int index) { + return mText.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return PrecomputedText.create(mText.subSequence(start, end), mParams); + } + + @Override + public String toString() { + return mText.toString(); + } +} diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index e62f4216f33a..299bde239fcf 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -651,42 +651,25 @@ public class StaticLayout extends Layout { b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE, indents, mLeftPaddings, mRightPaddings); - MeasuredText measured = null; + PrecomputedText measured = null; final Spanned spanned; - final boolean canUseMeasuredText; - if (source instanceof MeasuredText) { - measured = (MeasuredText) source; - - if (bufStart != measured.getStart() || bufEnd != measured.getEnd()) { - // The buffer position has changed. Re-measure here. - canUseMeasuredText = false; - } else if (b.mBreakStrategy != measured.getBreakStrategy() - || b.mHyphenationFrequency != measured.getHyphenationFrequency()) { - // The computed hyphenation pieces may not be able to used. Re-measure it. - canUseMeasuredText = false; - } else { - // We can use measured information. - canUseMeasuredText = true; + if (source instanceof PrecomputedText) { + measured = (PrecomputedText) source; + if (!measured.canUseMeasuredResult(bufStart, bufEnd, textDir, paint, b.mBreakStrategy, + b.mHyphenationFrequency)) { + // Some parameters are different from the ones when measured text is created. + measured = null; } - } else { - canUseMeasuredText = false; } - if (!canUseMeasuredText) { - measured = new MeasuredText.Builder(source, paint) - .setRange(bufStart, bufEnd) - .setTextDirection(textDir) - .setBreakStrategy(b.mBreakStrategy) - .setHyphenationFrequency(b.mHyphenationFrequency) - .build(false /* full layout is not necessary for line breaking */); + if (measured == null) { + final PrecomputedText.Params param = new PrecomputedText.Params(paint, textDir, + b.mBreakStrategy, b.mHyphenationFrequency); + measured = PrecomputedText.createWidthOnly(source, param, bufStart, bufEnd); spanned = (source instanceof Spanned) ? (Spanned) source : null; } else { final CharSequence original = measured.getText(); spanned = (original instanceof Spanned) ? (Spanned) original : null; - // Overwrite with the one when measured. - // TODO: Give an option for developer not to overwrite and measure again here? - textDir = measured.getTextDir(); - paint = measured.getPaint(); } try { diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java index 55367dcce47e..117a77d32997 100644 --- a/core/java/android/text/TextLine.java +++ b/core/java/android/text/TextLine.java @@ -60,7 +60,7 @@ public class TextLine { private char[] mChars; private boolean mCharsValid; private Spanned mSpanned; - private MeasuredText mMeasured; + private PrecomputedText mComputed; // Additional width of whitespace for justification. This value is per whitespace, thus // the line width will increase by mAddedWidth x (number of stretchable whitespaces). @@ -119,7 +119,7 @@ public class TextLine { tl.mSpanned = null; tl.mTabs = null; tl.mChars = null; - tl.mMeasured = null; + tl.mComputed = null; tl.mMetricAffectingSpanSpanSet.recycle(); tl.mCharacterStyleSpanSet.recycle(); @@ -170,12 +170,9 @@ public class TextLine { hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0; } - mMeasured = null; - if (text instanceof MeasuredText) { - MeasuredText mt = (MeasuredText) text; - if (mt.canUseMeasuredResult(paint)) { - mMeasured = mt; - } + mComputed = null; + if (text instanceof PrecomputedText) { + mComputed = (PrecomputedText) text; } mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT; @@ -746,12 +743,12 @@ public class TextLine { return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset); } else { final int delta = mStart; - if (mMeasured == null) { + if (mComputed == null) { // TODO: Enable measured getRunAdvance for ReplacementSpan and RTL text. return wp.getRunAdvance(mText, delta + start, delta + end, delta + contextStart, delta + contextEnd, runIsRtl, delta + offset); } else { - return mMeasured.getWidth(start + delta, end + delta); + return mComputed.getWidth(start + delta, end + delta); } } } diff --git a/core/java/android/view/RecordingCanvas.java b/core/java/android/view/RecordingCanvas.java index fbb862be54ef..fc7d828de12e 100644 --- a/core/java/android/view/RecordingCanvas.java +++ b/core/java/android/view/RecordingCanvas.java @@ -34,7 +34,7 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.TemporaryBuffer; import android.text.GraphicsOperations; -import android.text.MeasuredText; +import android.text.PrecomputedText; import android.text.SpannableString; import android.text.SpannedString; import android.text.TextUtils; @@ -507,8 +507,8 @@ public class RecordingCanvas extends Canvas { TextUtils.getChars(text, contextStart, contextEnd, buf, 0); long measuredTextPtr = 0; int measuredTextOffset = 0; - if (text instanceof MeasuredText) { - MeasuredText mt = (MeasuredText) text; + if (text instanceof PrecomputedText) { + PrecomputedText mt = (PrecomputedText) text; int paraIndex = mt.findParaIndex(start); if (end <= mt.getParagraphEnd(paraIndex)) { // Only support if the target is in the same paragraph. @@ -641,7 +641,7 @@ public class RecordingCanvas extends Canvas { @FastNative private static native void nDrawTextRun(long nativeCanvas, char[] text, int start, int count, int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint, - long nativeMeasuredText, int measuredTextOffset); + long nativePrecomputedText, int measuredTextOffset); @FastNative private static native void nDrawTextOnPath(long nativeCanvas, char[] text, int index, int count, diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 1e02c3062d97..8aae0d865cd2 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -80,8 +80,8 @@ import android.text.GraphicsOperations; import android.text.InputFilter; import android.text.InputType; import android.text.Layout; -import android.text.MeasuredText; import android.text.ParcelableSpan; +import android.text.PrecomputedText; import android.text.Selection; import android.text.SpanWatcher; import android.text.Spannable; @@ -4092,6 +4092,35 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Gets the parameters for text layout precomputation, for use with {@link PrecomputedText}. + * + * @return a current {@link PrecomputedText.Params} + * @see PrecomputedText + */ + public @NonNull PrecomputedText.Params getTextMetricsParams() { + return new PrecomputedText.Params(new TextPaint(mTextPaint), getTextDirectionHeuristic(), + mBreakStrategy, mHyphenationFrequency); + } + + /** + * Apply the text layout parameter. + * + * Update the TextView parameters to be compatible with {@link PrecomputedText.Params}. + * @see PrecomputedText + */ + public void setTextMetricsParams(@NonNull PrecomputedText.Params params) { + mTextPaint.set(params.getTextPaint()); + mTextDir = params.getTextDirection(); + mBreakStrategy = params.getBreakStrategy(); + mHyphenationFrequency = params.getHyphenationFrequency(); + if (mLayout != null) { + nullLayouts(); + requestLayout(); + invalidate(); + } + } + + /** * Set justification mode. The default value is {@link Layout#JUSTIFICATION_MODE_NONE}. If the * last line is too short for justification, the last line will be displayed with the * alignment set by {@link android.view.View#setTextAlignment}. @@ -5584,7 +5613,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (imm != null) imm.restartInput(this); } else if (type == BufferType.SPANNABLE || mMovement != null) { text = mSpannableFactory.newSpannable(text); - } else if (!(text instanceof MeasuredText || text instanceof CharWrapper)) { + } else if (!(text instanceof PrecomputedText || text instanceof CharWrapper)) { text = TextUtils.stringOrSpannedString(text); } @@ -11712,6 +11741,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Returns the current {@link TextDirectionHeuristic}. + * + * @return the current {@link TextDirectionHeuristic}. * @hide */ protected TextDirectionHeuristic getTextDirectionHeuristic() { |
