| /* |
| * 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.FloatRange; |
| import android.annotation.IntDef; |
| import android.annotation.IntRange; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.graphics.Paint; |
| import android.graphics.Rect; |
| import android.graphics.text.LineBreakConfig; |
| import android.graphics.text.MeasuredText; |
| import android.text.style.MetricAffectingSpan; |
| |
| import com.android.internal.util.Preconditions; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| 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> |
| * static void asyncSetText(TextView textView, final String longString, Executor bgExecutor) { |
| * // construct precompute related parameters using the TextView that we will set the text on. |
| * final PrecomputedText.Params params = textView.getTextMetricsParams(); |
| * final Reference textViewRef = new WeakReference<>(textView); |
| * bgExecutor.submit(() -> { |
| * TextView textView = textViewRef.get(); |
| * if (textView == null) return; |
| * final PrecomputedText precomputedText = PrecomputedText.create(longString, params); |
| * textView.post(() -> { |
| * TextView textView = textViewRef.get(); |
| * if (textView == null) return; |
| * textView.setText(precomputedText); |
| * }); |
| * }); |
| * } |
| * </code> |
| * </pre> |
| * |
| * Note that the {@link PrecomputedText} created from different parameters of the target |
| * {@link android.widget.TextView} will be rejected. |
| * |
| * Note that any {@link android.text.NoCopySpan} attached to the original text won't be passed to |
| * PrecomputedText. |
| */ |
| public class PrecomputedText implements Spannable { |
| 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; |
| |
| // The line break configuration for calculating text wrapping. |
| private final @NonNull LineBreakConfig mLineBreakConfig; |
| |
| /** |
| * 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; |
| |
| // The line break configuration for calculating text wrapping. |
| private @NonNull LineBreakConfig mLineBreakConfig = LineBreakConfig.NONE; |
| |
| /** |
| * Builder constructor. |
| * |
| * @param paint the paint to be used for drawing |
| */ |
| public Builder(@NonNull TextPaint paint) { |
| mPaint = paint; |
| } |
| |
| /** |
| * Builder constructor from existing params. |
| */ |
| public Builder(@NonNull Params params) { |
| mPaint = params.mPaint; |
| mTextDir = params.mTextDir; |
| mBreakStrategy = params.mBreakStrategy; |
| mHyphenationFrequency = params.mHyphenationFrequency; |
| mLineBreakConfig = params.mLineBreakConfig; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * Set the line break config for the text wrapping. |
| * |
| * @param lineBreakConfig the newly line break configuration. |
| * @return this builder, useful for chaining. |
| * @see StaticLayout.Builder#setLineBreakConfig |
| */ |
| public @NonNull Builder setLineBreakConfig(@NonNull LineBreakConfig lineBreakConfig) { |
| mLineBreakConfig = lineBreakConfig; |
| return this; |
| } |
| |
| /** |
| * Build the {@link Params}. |
| * |
| * @return the layout parameter |
| */ |
| public @NonNull Params build() { |
| return new Params(mPaint, mLineBreakConfig, mTextDir, mBreakStrategy, |
| mHyphenationFrequency); |
| } |
| } |
| |
| // This is public hidden for internal use. |
| // For the external developers, use Builder instead. |
| /** @hide */ |
| public Params(@NonNull TextPaint paint, |
| @NonNull LineBreakConfig lineBreakConfig, |
| @NonNull TextDirectionHeuristic textDir, |
| @Layout.BreakStrategy int strategy, |
| @Layout.HyphenationFrequency int frequency) { |
| mPaint = paint; |
| mTextDir = textDir; |
| mBreakStrategy = strategy; |
| mHyphenationFrequency = frequency; |
| mLineBreakConfig = lineBreakConfig; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * Returns the {@link LineBreakConfig} for this text. |
| * |
| * @return the current line break configuration. The {@link LineBreakConfig} with default |
| * values will be returned if no line break configuration is set. |
| */ |
| public @NonNull LineBreakConfig getLineBreakConfig() { |
| return mLineBreakConfig; |
| } |
| |
| /** @hide */ |
| @IntDef(value = { UNUSABLE, NEED_RECOMPUTE, USABLE }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface CheckResultUsableResult {} |
| |
| /** |
| * Constant for returning value of checkResultUsable indicating that given parameter is not |
| * compatible. |
| * @hide |
| */ |
| public static final int UNUSABLE = 0; |
| |
| /** |
| * Constant for returning value of checkResultUsable indicating that given parameter is not |
| * compatible but partially usable for creating new PrecomputedText. |
| * @hide |
| */ |
| public static final int NEED_RECOMPUTE = 1; |
| |
| /** |
| * Constant for returning value of checkResultUsable indicating that given parameter is |
| * compatible. |
| * @hide |
| */ |
| public static final int USABLE = 2; |
| |
| /** @hide */ |
| public @CheckResultUsableResult int checkResultUsable(@NonNull TextPaint paint, |
| @NonNull TextDirectionHeuristic textDir, @Layout.BreakStrategy int strategy, |
| @Layout.HyphenationFrequency int frequency, @NonNull LineBreakConfig lbConfig) { |
| if (mBreakStrategy == strategy && mHyphenationFrequency == frequency |
| && mLineBreakConfig.equals(lbConfig) |
| && mPaint.equalsForTextMeasurement(paint)) { |
| return mTextDir == textDir ? USABLE : NEED_RECOMPUTE; |
| } else { |
| return UNUSABLE; |
| } |
| } |
| |
| /** |
| * 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 checkResultUsable(param.mPaint, param.mTextDir, param.mBreakStrategy, |
| param.mHyphenationFrequency, param.mLineBreakConfig) == Params.USABLE; |
| } |
| |
| @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, |
| LineBreakConfig.getResolvedLineBreakStyle(mLineBreakConfig), |
| LineBreakConfig.getResolvedLineBreakWordStyle(mLineBreakConfig)); |
| } |
| |
| @Override |
| public String toString() { |
| return "{" |
| + "textSize=" + mPaint.getTextSize() |
| + ", textScaleX=" + mPaint.getTextScaleX() |
| + ", textSkewX=" + mPaint.getTextSkewX() |
| + ", letterSpacing=" + mPaint.getLetterSpacing() |
| + ", textLocale=" + mPaint.getTextLocales() |
| + ", typeface=" + mPaint.getTypeface() |
| + ", variationSettings=" + mPaint.getFontVariationSettings() |
| + ", elegantTextHeight=" + mPaint.isElegantTextHeight() |
| + ", textDir=" + mTextDir |
| + ", breakStrategy=" + mBreakStrategy |
| + ", hyphenationFrequency=" + mHyphenationFrequency |
| + ", lineBreakStyle=" + LineBreakConfig.getResolvedLineBreakStyle(mLineBreakConfig) |
| + ", lineBreakWordStyle=" |
| + LineBreakConfig.getResolvedLineBreakWordStyle(mLineBreakConfig) |
| + "}"; |
| } |
| }; |
| |
| /** @hide */ |
| public static class ParagraphInfo { |
| public final @IntRange(from = 0) int paragraphEnd; |
| public final @NonNull MeasuredParagraph measured; |
| |
| /** |
| * @param paraEnd the end offset of this paragraph |
| * @param measured a measured paragraph |
| */ |
| public ParagraphInfo(@IntRange(from = 0) int paraEnd, @NonNull MeasuredParagraph measured) { |
| this.paragraphEnd = paraEnd; |
| this.measured = measured; |
| } |
| }; |
| |
| |
| // The original text. |
| private final @NonNull SpannableString 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 list of measured paragraph info. |
| private final @NonNull ParagraphInfo[] mParagraphInfo; |
| |
| /** |
| * 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> |
| * |
| * Note that any {@link android.text.NoCopySpan} attached to the text won't be passed to the |
| * created PrecomputedText. |
| * |
| * @param text the text to be measured |
| * @param params parameters that define how text will be precomputed |
| * @return A {@link PrecomputedText} |
| */ |
| public static PrecomputedText create(@NonNull CharSequence text, @NonNull Params params) { |
| ParagraphInfo[] paraInfo = null; |
| if (text instanceof PrecomputedText) { |
| final PrecomputedText hintPct = (PrecomputedText) text; |
| final PrecomputedText.Params hintParams = hintPct.getParams(); |
| final @Params.CheckResultUsableResult int checkResult = |
| hintParams.checkResultUsable(params.mPaint, params.mTextDir, |
| params.mBreakStrategy, params.mHyphenationFrequency, |
| params.mLineBreakConfig); |
| switch (checkResult) { |
| case Params.USABLE: |
| return hintPct; |
| case Params.NEED_RECOMPUTE: |
| // To be able to use PrecomputedText for new params, at least break strategy and |
| // hyphenation frequency must be the same. |
| if (params.getBreakStrategy() == hintParams.getBreakStrategy() |
| && params.getHyphenationFrequency() |
| == hintParams.getHyphenationFrequency()) { |
| paraInfo = createMeasuredParagraphsFromPrecomputedText( |
| hintPct, params, true /* compute layout */); |
| } |
| break; |
| case Params.UNUSABLE: |
| // Unable to use anything in PrecomputedText. Create PrecomputedText as the |
| // normal text input. |
| } |
| |
| } |
| if (paraInfo == null) { |
| paraInfo = createMeasuredParagraphs( |
| text, params, 0, text.length(), true /* computeLayout */, |
| true /* computeBounds */); |
| } |
| return new PrecomputedText(text, 0, text.length(), params, paraInfo); |
| } |
| |
| private static boolean isFastHyphenation(int frequency) { |
| return frequency == Layout.HYPHENATION_FREQUENCY_FULL_FAST |
| || frequency == Layout.HYPHENATION_FREQUENCY_NORMAL_FAST; |
| } |
| |
| private static ParagraphInfo[] createMeasuredParagraphsFromPrecomputedText( |
| @NonNull PrecomputedText pct, @NonNull Params params, boolean computeLayout) { |
| final boolean needHyphenation = params.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE |
| && params.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE; |
| final int hyphenationMode; |
| if (needHyphenation) { |
| hyphenationMode = isFastHyphenation(params.getHyphenationFrequency()) |
| ? MeasuredText.Builder.HYPHENATION_MODE_FAST : |
| MeasuredText.Builder.HYPHENATION_MODE_NORMAL; |
| } else { |
| hyphenationMode = MeasuredText.Builder.HYPHENATION_MODE_NONE; |
| } |
| LineBreakConfig config = params.getLineBreakConfig(); |
| if (config.getLineBreakWordStyle() == LineBreakConfig.LINE_BREAK_WORD_STYLE_AUTO |
| && pct.getParagraphCount() != 1) { |
| // If the text has multiple paragraph, resolve line break word style auto to none. |
| config = new LineBreakConfig.Builder() |
| .merge(config) |
| .setLineBreakWordStyle(LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE) |
| .build(); |
| } |
| ArrayList<ParagraphInfo> result = new ArrayList<>(); |
| for (int i = 0; i < pct.getParagraphCount(); ++i) { |
| final int paraStart = pct.getParagraphStart(i); |
| final int paraEnd = pct.getParagraphEnd(i); |
| result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout( |
| params.getTextPaint(), config, pct, paraStart, paraEnd, |
| params.getTextDirection(), hyphenationMode, computeLayout, true, |
| pct.getMeasuredParagraph(i), null /* no recycle */))); |
| } |
| return result.toArray(new ParagraphInfo[result.size()]); |
| } |
| |
| /** @hide */ |
| public static ParagraphInfo[] createMeasuredParagraphs( |
| @NonNull CharSequence text, @NonNull Params params, |
| @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean computeLayout, |
| boolean computeBounds) { |
| ArrayList<ParagraphInfo> result = new ArrayList<>(); |
| |
| Preconditions.checkNotNull(text); |
| Preconditions.checkNotNull(params); |
| final boolean needHyphenation = params.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE |
| && params.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE; |
| final int hyphenationMode; |
| if (needHyphenation) { |
| hyphenationMode = isFastHyphenation(params.getHyphenationFrequency()) |
| ? MeasuredText.Builder.HYPHENATION_MODE_FAST : |
| MeasuredText.Builder.HYPHENATION_MODE_NORMAL; |
| } else { |
| hyphenationMode = MeasuredText.Builder.HYPHENATION_MODE_NONE; |
| } |
| |
| LineBreakConfig config = null; |
| 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. |
| } |
| |
| if (config == null) { |
| config = params.getLineBreakConfig(); |
| if (config.getLineBreakWordStyle() == LineBreakConfig.LINE_BREAK_WORD_STYLE_AUTO |
| && !(paraStart == start && paraEnd == end)) { |
| // If the text has multiple paragraph, resolve line break word style auto to |
| // none. |
| config = new LineBreakConfig.Builder() |
| .merge(config) |
| .setLineBreakWordStyle(LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE) |
| .build(); |
| } |
| } |
| |
| result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout( |
| params.getTextPaint(), config, text, paraStart, paraEnd, |
| params.getTextDirection(), hyphenationMode, computeLayout, computeBounds, |
| null /* no hint */, |
| null /* no recycle */))); |
| } |
| return result.toArray(new ParagraphInfo[result.size()]); |
| } |
| |
| // Use PrecomputedText.create instead. |
| private PrecomputedText(@NonNull CharSequence text, @IntRange(from = 0) int start, |
| @IntRange(from = 0) int end, @NonNull Params params, |
| @NonNull ParagraphInfo[] paraInfo) { |
| mText = new SpannableString(text, true /* ignoreNoCopySpan */); |
| mStart = start; |
| mEnd = end; |
| mParams = params; |
| mParagraphInfo = paraInfo; |
| } |
| |
| /** |
| * Return the underlying text. |
| * @hide |
| */ |
| 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 mParagraphInfo.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 : getParagraphEnd(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 mParagraphInfo[paraIndex].paragraphEnd; |
| } |
| |
| /** @hide */ |
| public @NonNull MeasuredParagraph getMeasuredParagraph(@IntRange(from = 0) int paraIndex) { |
| return mParagraphInfo[paraIndex].measured; |
| } |
| |
| /** @hide */ |
| public @NonNull ParagraphInfo[] getParagraphInfo() { |
| return mParagraphInfo; |
| } |
| |
| /** |
| * Returns true if the given TextPaint gives the same result of text layout for this text. |
| * @hide |
| */ |
| public @Params.CheckResultUsableResult int checkResultUsable(@IntRange(from = 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) { |
| if (mStart != start || mEnd != end) { |
| return Params.UNUSABLE; |
| } else { |
| return mParams.checkResultUsable(paint, textDir, strategy, frequency, lbConfig); |
| } |
| } |
| |
| /** @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 < mParagraphInfo.length; ++i) { |
| if (pos < mParagraphInfo[i].paragraphEnd) { |
| return i; |
| } |
| } |
| throw new IndexOutOfBoundsException( |
| "pos must be less than " + mParagraphInfo[mParagraphInfo.length - 1].paragraphEnd |
| + ", gave " + pos); |
| } |
| |
| /** |
| * Returns text width for the given range. |
| * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise |
| * IllegalArgumentException will be thrown. |
| * |
| * @param start the inclusive start offset in the text |
| * @param end the exclusive end offset in the text |
| * @return the text width |
| * @throws IllegalArgumentException if start and end offset are in the different paragraph. |
| */ |
| public @FloatRange(from = 0) float getWidth(@IntRange(from = 0) int start, |
| @IntRange(from = 0) int end) { |
| Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset"); |
| Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset"); |
| Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset"); |
| |
| if (start == end) { |
| return 0; |
| } |
| final int paraIndex = findParaIndex(start); |
| final int paraStart = getParagraphStart(paraIndex); |
| final int paraEnd = getParagraphEnd(paraIndex); |
| if (start < paraStart || paraEnd < end) { |
| throw new IllegalArgumentException("Cannot measured across the paragraph:" |
| + "para: (" + paraStart + ", " + paraEnd + "), " |
| + "request: (" + start + ", " + end + ")"); |
| } |
| return getMeasuredParagraph(paraIndex).getWidth(start - paraStart, end - paraStart); |
| } |
| |
| /** |
| * Retrieves the text bounding box for the given range. |
| * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise |
| * IllegalArgumentException will be thrown. |
| * |
| * @param start the inclusive start offset in the text |
| * @param end the exclusive end offset in the text |
| * @param bounds the output rectangle |
| * @throws IllegalArgumentException if start and end offset are in the different paragraph. |
| */ |
| public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end, |
| @NonNull Rect bounds) { |
| Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset"); |
| Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset"); |
| Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset"); |
| Preconditions.checkNotNull(bounds); |
| if (start == end) { |
| bounds.set(0, 0, 0, 0); |
| return; |
| } |
| final int paraIndex = findParaIndex(start); |
| final int paraStart = getParagraphStart(paraIndex); |
| final int paraEnd = getParagraphEnd(paraIndex); |
| if (start < paraStart || paraEnd < end) { |
| throw new IllegalArgumentException("Cannot measured across the paragraph:" |
| + "para: (" + paraStart + ", " + paraEnd + "), " |
| + "request: (" + start + ", " + end + ")"); |
| } |
| getMeasuredParagraph(paraIndex).getBounds(start - paraStart, end - paraStart, bounds); |
| } |
| |
| /** |
| * Retrieves the text font metrics for the given range. |
| * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise |
| * IllegalArgumentException will be thrown. |
| * |
| * @param start the inclusive start offset in the text |
| * @param end the exclusive end offset in the text |
| * @param outMetrics the output font metrics |
| * @throws IllegalArgumentException if start and end offset are in the different paragraph. |
| */ |
| public void getFontMetricsInt(@IntRange(from = 0) int start, @IntRange(from = 0) int end, |
| @NonNull Paint.FontMetricsInt outMetrics) { |
| Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset"); |
| Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset"); |
| Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset"); |
| Objects.requireNonNull(outMetrics); |
| if (start == end) { |
| mParams.getTextPaint().getFontMetricsInt(outMetrics); |
| return; |
| } |
| final int paraIndex = findParaIndex(start); |
| final int paraStart = getParagraphStart(paraIndex); |
| final int paraEnd = getParagraphEnd(paraIndex); |
| if (start < paraStart || paraEnd < end) { |
| throw new IllegalArgumentException("Cannot measured across the paragraph:" |
| + "para: (" + paraStart + ", " + paraEnd + "), " |
| + "request: (" + start + ", " + end + ")"); |
| } |
| getMeasuredParagraph(paraIndex).getFontMetricsInt(start - paraStart, |
| end - paraStart, outMetrics); |
| } |
| |
| /** |
| * Returns a width of a character at offset |
| * |
| * @param offset an offset of the text. |
| * @return a width of the character. |
| * @hide |
| */ |
| public float getCharWidthAt(@IntRange(from = 0) int offset) { |
| Preconditions.checkArgument(0 <= offset && offset < mText.length(), "invalid offset"); |
| final int paraIndex = findParaIndex(offset); |
| final int paraStart = getParagraphStart(paraIndex); |
| final int paraEnd = getParagraphEnd(paraIndex); |
| return getMeasuredParagraph(paraIndex).getCharWidthAt(offset - 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; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////////////////////// |
| // Spannable overrides |
| // |
| // Do not allow to modify MetricAffectingSpan |
| |
| /** |
| * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified. |
| */ |
| @Override |
| public void setSpan(Object what, int start, int end, int flags) { |
| if (what instanceof MetricAffectingSpan) { |
| throw new IllegalArgumentException( |
| "MetricAffectingSpan can not be set to PrecomputedText."); |
| } |
| mText.setSpan(what, start, end, flags); |
| } |
| |
| /** |
| * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified. |
| */ |
| @Override |
| public void removeSpan(Object what) { |
| if (what instanceof MetricAffectingSpan) { |
| throw new IllegalArgumentException( |
| "MetricAffectingSpan can not be removed from PrecomputedText."); |
| } |
| mText.removeSpan(what); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////////////////////// |
| // 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(); |
| } |
| } |