/* * 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.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Rect; import android.text.style.MetricAffectingSpan; 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. * *
* An example usage is:
*
* 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);
* });
* });
* }
*
*
*
* 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;
/**
* 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;
}
/** @hide */
public 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);
}
@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
+ "}";
}
};
/** @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.
* * This can be expensive, so computing this on a background thread before your text will be * presented can save work on the UI thread. *
* * 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 = createMeasuredParagraphs( text, params, 0, text.length(), true /* computeLayout */); return new PrecomputedText(text, 0, text.length(), params, paraInfo); } /** @hide */ public static ParagraphInfo[] createMeasuredParagraphs( @NonNull CharSequence text, @NonNull Params params, @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean computeLayout) { ArrayList