845 lines
33 KiB
Java
845 lines
33 KiB
Java
![]() |
/*
|
||
|
* 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();
|
||
|
}
|
||
|
}
|