blob: 90d69e41fc8aa834ae72082af8d04e6f18481256 [file] [log] [blame]
/*
* Copyright 2018 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 androidx.core.text;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
import android.annotation.SuppressLint;
import android.os.Build;
import android.text.Layout;
import android.text.PrecomputedText;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.StaticLayout;
import android.text.TextDirectionHeuristic;
import android.text.TextDirectionHeuristics;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.MetricAffectingSpan;
import androidx.annotation.GuardedBy;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.UiThread;
import androidx.core.os.TraceCompat;
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Preconditions;
import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
/**
* 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 PrecomputedTextCompat 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.
*
* On API 29 or later, there is full PrecomputedText support by framework. From API 21 to API 27,
* PrecomputedTextCompat relies on internal text layout cache. PrecomputedTextCompat immediately
* computes the text layout in the constuctor to warm up the internal text layout cache. On API 20
* or before, PrecomputedTextCompat does nothing.
*
* Note that any {@link android.text.NoCopySpan} attached to the original text won't be passed to
* PrecomputedText.
*/
public class PrecomputedTextCompat implements Spannable {
private static final char LINE_FEED = '\n';
private static final Object sLock = new Object();
@GuardedBy("sLock") private static @NonNull Executor sExecutor = null;
/**
* The information required for building {@link PrecomputedTextCompat}.
*
* 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 {
private final @NonNull TextPaint mPaint;
// null on API 17 or before, non null on API 18 or later.
private final @Nullable TextDirectionHeuristic mTextDir;
private final int mBreakStrategy;
private final int mHyphenationFrequency;
final PrecomputedText.Params mWrapped;
/**
* 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;
// The break strategy for this measured text.
private int mBreakStrategy;
// The hyphenation frequency for this measured text.
private int mHyphenationFrequency;
/**
* Builder constructor.
*
* @param paint the paint to be used for drawing
*/
public Builder(@NonNull TextPaint paint) {
mPaint = paint;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NORMAL;
} else {
mBreakStrategy = mHyphenationFrequency = 0;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
} else {
mTextDir = null;
}
}
/**
* Set the line break strategy.
*
* The default value is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}.
*
* On API 22 and below, this has no effect as there is no line break strategy.
*
* @param strategy the break strategy
* @return PrecomputedTextCompat.Builder instance
* @see StaticLayout.Builder#setBreakStrategy
* @see android.widget.TextView#setBreakStrategy
*/
@RequiresApi(23)
public Builder setBreakStrategy(int strategy) {
mBreakStrategy = strategy;
return this;
}
/**
* Set the hyphenation frequency.
*
* The default value is {@link Layout#HYPHENATION_FREQUENCY_NORMAL}.
*
* On API 22 and below, this has no effect as there is no hyphenation frequency.
*
* @param frequency the hyphenation frequency
* @return PrecomputedTextCompat.Builder instance
* @see StaticLayout.Builder#setHyphenationFrequency
* @see android.widget.TextView#setHyphenationFrequency
*/
@RequiresApi(23)
public Builder setHyphenationFrequency(int frequency) {
mHyphenationFrequency = frequency;
return this;
}
/**
* Set the text direction heuristic.
*
* The default value is {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}.
*
* On API 17 or before, text direction heuristics cannot be modified, so this method
* does nothing.
*
* @param textDir the text direction heuristic for resolving bidi behavior
* @return PrecomputedTextCompat.Builder instance
* @see StaticLayout.Builder#setTextDirection
*/
@RequiresApi(18)
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);
}
}
@SuppressLint("NewApi") // TODO: Remove once Q SDK is released
Params(@NonNull TextPaint paint, @NonNull TextDirectionHeuristic textDir,
int strategy, int frequency) {
if (Build.VERSION.SDK_INT >= 29) {
mWrapped = new PrecomputedText.Params.Builder(paint)
.setBreakStrategy(strategy)
.setHyphenationFrequency(frequency)
.setTextDirection(textDir)
.build();
} else {
mWrapped = null;
}
mPaint = paint;
mTextDir = textDir;
mBreakStrategy = strategy;
mHyphenationFrequency = frequency;
}
@RequiresApi(28)
public Params(@NonNull PrecomputedText.Params wrapped) {
mPaint = wrapped.getTextPaint();
mTextDir = wrapped.getTextDirection();
mBreakStrategy = wrapped.getBreakStrategy();
mHyphenationFrequency = wrapped.getHyphenationFrequency();
mWrapped = (Build.VERSION.SDK_INT >= 29) ? wrapped : null;
}
/**
* Returns the {@link TextPaint} for this text.
*
* @return A {@link TextPaint}
*/
public @NonNull TextPaint getTextPaint() {
return mPaint;
}
/**
* Returns the {@link TextDirectionHeuristic} for this text.
*
* On API 17 and below, this returns null, otherwise returns non-null
* TextDirectionHeuristic.
*
* @return the {@link TextDirectionHeuristic}
*/
@RequiresApi(18)
public @Nullable TextDirectionHeuristic getTextDirection() {
return mTextDir;
}
/**
* Returns the break strategy for this text.
*
* On API 22 and below, this returns 0.
*
* @return the line break strategy
*/
@RequiresApi(23)
public int getBreakStrategy() {
return mBreakStrategy;
}
/**
* Returns the hyphenation frequency for this text.
*
* On API 22 and below, this returns 0.
*
* @return the hyphenation frequency
*/
@RequiresApi(23)
public int getHyphenationFrequency() {
return mHyphenationFrequency;
}
/**
* Similar to equals but don't compare text direction
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public boolean equalsWithoutTextDirection(@NonNull Params other) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (mBreakStrategy != other.getBreakStrategy()) {
return false;
}
if (mHyphenationFrequency != other.getHyphenationFrequency()) {
return false;
}
}
if (mPaint.getTextSize() != other.getTextPaint().getTextSize()) {
return false;
}
if (mPaint.getTextScaleX() != other.getTextPaint().getTextScaleX()) {
return false;
}
if (mPaint.getTextSkewX() != other.getTextPaint().getTextSkewX()) {
return false;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (mPaint.getLetterSpacing() != other.getTextPaint().getLetterSpacing()) {
return false;
}
if (!TextUtils.equals(mPaint.getFontFeatureSettings(),
other.getTextPaint().getFontFeatureSettings())) {
return false;
}
}
if (mPaint.getFlags() != other.getTextPaint().getFlags()) {
return false;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (!mPaint.getTextLocales().equals(other.getTextPaint().getTextLocales())) {
return false;
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
if (!mPaint.getTextLocale().equals(other.getTextPaint().getTextLocale())) {
return false;
}
}
if (mPaint.getTypeface() == null) {
if (other.getTextPaint().getTypeface() != null) {
return false;
}
} else if (!mPaint.getTypeface().equals(other.getTextPaint().getTypeface())) {
return false;
}
return true;
}
/**
* 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 instanceof Params)) {
return false;
}
Params other = (Params) o;
if (!equalsWithoutTextDirection(other)) {
return false;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
if (mTextDir != other.getTextDirection()) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return ObjectsCompat.hash(mPaint.getTextSize(), mPaint.getTextScaleX(),
mPaint.getTextSkewX(), mPaint.getLetterSpacing(), mPaint.getFlags(),
mPaint.getTextLocales(), mPaint.getTypeface(), mPaint.isElegantTextHeight(),
mTextDir, mBreakStrategy, mHyphenationFrequency);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return ObjectsCompat.hash(mPaint.getTextSize(), mPaint.getTextScaleX(),
mPaint.getTextSkewX(), mPaint.getLetterSpacing(), mPaint.getFlags(),
mPaint.getTextLocale(), mPaint.getTypeface(), mPaint.isElegantTextHeight(),
mTextDir, mBreakStrategy, mHyphenationFrequency);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
return ObjectsCompat.hash(mPaint.getTextSize(), mPaint.getTextScaleX(),
mPaint.getTextSkewX(), mPaint.getFlags(), mPaint.getTextLocale(),
mPaint.getTypeface(), mTextDir, mBreakStrategy, mHyphenationFrequency);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
return ObjectsCompat.hash(mPaint.getTextSize(), mPaint.getTextScaleX(),
mPaint.getTextSkewX(), mPaint.getFlags(), mPaint.getTextLocale(),
mPaint.getTypeface(), mTextDir, mBreakStrategy, mHyphenationFrequency);
} else {
return ObjectsCompat.hash(mPaint.getTextSize(), mPaint.getTextScaleX(),
mPaint.getTextSkewX(), mPaint.getFlags(), mPaint.getTypeface(), mTextDir,
mBreakStrategy, mHyphenationFrequency);
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("{");
sb.append("textSize=" + mPaint.getTextSize());
sb.append(", textScaleX=" + mPaint.getTextScaleX());
sb.append(", textSkewX=" + mPaint.getTextSkewX());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
sb.append(", letterSpacing=" + mPaint.getLetterSpacing());
sb.append(", elegantTextHeight=" + mPaint.isElegantTextHeight());
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
sb.append(", textLocale=" + mPaint.getTextLocales());
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
sb.append(", textLocale=" + mPaint.getTextLocale());
}
sb.append(", typeface=" + mPaint.getTypeface());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
sb.append(", variationSettings=" + mPaint.getFontVariationSettings());
}
sb.append(", textDir=" + mTextDir);
sb.append(", breakStrategy=" + mBreakStrategy);
sb.append(", hyphenationFrequency=" + mHyphenationFrequency);
sb.append("}");
return sb.toString();
}
};
// The original text.
private final @NonNull Spannable mText;
private final @NonNull Params mParams;
// The list of measured paragraph info.
private final @NonNull int[] mParagraphEnds;
// null on API 27 or before. Non-null on API 29 or later
private final @Nullable PrecomputedText mWrapped;
/**
* 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}
*/
@SuppressLint("NewApi") // TODO: Remove once Q SDK is released
public static PrecomputedTextCompat create(@NonNull CharSequence text, @NonNull Params params) {
Preconditions.checkNotNull(text);
Preconditions.checkNotNull(params);
try {
TraceCompat.beginSection("PrecomputedText");
if (Build.VERSION.SDK_INT >= 29 && params.mWrapped != null) {
return new PrecomputedTextCompat(
PrecomputedText.create(text, params.mWrapped), params);
}
ArrayList<Integer> ends = new ArrayList<>();
int paraEnd = 0;
int end = text.length();
for (int paraStart = 0; 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.
}
ends.add(paraEnd);
}
int[] result = new int[ends.size()];
for (int i = 0; i < ends.size(); ++i) {
result[i] = ends.get(i);
}
// No framework support for PrecomputedText
// Compute text layout and throw away StaticLayout for the purpose of warming up the
// internal text layout cache.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
StaticLayout.Builder.obtain(text, 0, text.length(), params.getTextPaint(),
Integer.MAX_VALUE)
.setBreakStrategy(params.getBreakStrategy())
.setHyphenationFrequency(params.getHyphenationFrequency())
.setTextDirection(params.getTextDirection())
.build();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
new StaticLayout(text, params.getTextPaint(), Integer.MAX_VALUE,
Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
} else {
// There is no way of precomputing text layout on API 20 or before
// Do nothing
}
return new PrecomputedTextCompat(text, params, result);
} finally {
TraceCompat.endSection();
}
}
// Use PrecomputedText.create instead.
private PrecomputedTextCompat(@NonNull CharSequence text, @NonNull Params params,
@NonNull int[] paraEnds) {
mText = new SpannableString(text);
mParams = params;
mParagraphEnds = paraEnds;
mWrapped = null;
}
@RequiresApi(28)
private PrecomputedTextCompat(@NonNull PrecomputedText precomputed, @NonNull Params params) {
mText = precomputed;
mParams = params;
mParagraphEnds = null;
mWrapped = (Build.VERSION.SDK_INT >= 29) ? precomputed : null;
}
/**
* Returns the underlying original text if the text is PrecomputedText.
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@RequiresApi(28)
public @Nullable PrecomputedText getPrecomputedText() {
if (mText instanceof PrecomputedText) {
return (PrecomputedText) mText;
} else {
return null;
}
}
/**
* Returns the parameters used to measure this text.
*/
public @NonNull Params getParams() {
return mParams;
}
/**
* Returns the count of paragraphs.
*/
@SuppressLint("NewApi") // TODO: Remove once Q SDK is released
public @IntRange(from = 0) int getParagraphCount() {
if (Build.VERSION.SDK_INT >= 29) {
return mWrapped.getParagraphCount();
} else {
return mParagraphEnds.length;
}
}
/**
* Returns the paragraph start offset of the text.
*/
@SuppressLint("NewApi") // TODO: Remove once Q SDK is released
public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) {
Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
if (Build.VERSION.SDK_INT >= 29) {
return mWrapped.getParagraphStart(paraIndex);
} else {
return paraIndex == 0 ? 0 : mParagraphEnds[paraIndex - 1];
}
}
/**
* Returns the paragraph end offset of the text.
*/
@SuppressLint("NewApi") // TODO: Remove once Q SDK is released
public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) {
Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
if (Build.VERSION.SDK_INT >= 29) {
return mWrapped.getParagraphEnd(paraIndex);
} else {
return mParagraphEnds[paraIndex];
}
}
/**
* A helper class for computing text layout in background
*/
private static class PrecomputedTextFutureTask extends FutureTask<PrecomputedTextCompat> {
private static class PrecomputedTextCallback implements Callable<PrecomputedTextCompat> {
private PrecomputedTextCompat.Params mParams;
private CharSequence mText;
PrecomputedTextCallback(@NonNull final PrecomputedTextCompat.Params params,
@NonNull final CharSequence cs) {
mParams = params;
mText = cs;
}
@Override
public PrecomputedTextCompat call() throws Exception {
return PrecomputedTextCompat.create(mText, mParams);
}
}
PrecomputedTextFutureTask(@NonNull final PrecomputedTextCompat.Params params,
@NonNull final CharSequence text) {
super(new PrecomputedTextCallback(params, text));
}
}
/**
* Helper for PrecomputedText that returns a future to be used with
* {@link androidx.appcompat.widget.AppCompatTextView#setTextFuture}.
*
* PrecomputedText is suited to compute on a background thread, but when TextView properties are
* dynamic, it's common to configure text properties and text at the same time, when binding a
* View. For example, in a RecyclerView Adapter:
* <pre>
* void onBindViewHolder(ViewHolder vh, int position) {
* ItemData data = getData(position);
*
* vh.textView.setTextSize(...);
* vh.textView.setFontVariationSettings(...);
* vh.textView.setText(data.text);
* }
* </pre>
* In such cases, using PrecomputedText is difficult, since it isn't safe to defer the setText()
* code arbitrarily - a layout pass may happen before computation finishes, and will be
* incorrect if the text isn't ready yet.
* <p>
* With {@code getTextFuture()}, you can block on the result of the precomputation safely
* before the result is needed. AppCompatTextView provides
* {@link androidx.appcompat.widget.AppCompatTextView#setTextFuture} for exactly this
* use case. With the following code, the app's layout work is largely done on a background
* thread:
* <pre>
* void onBindViewHolder(ViewHolder vh, int position) {
* ItemData data = getData(position);
*
* vh.textView.setTextSize(...);
* vh.textView.setFontVariationSettings(...);
*
* // start precompute
* Future<PrecomputedTextCompat> future = PrecomputedTextCompat.getTextFuture(
* data.text, vh.textView.getTextMetricsParamsCompat(), myExecutor);
*
* // and pass future to TextView, which awaits result before measuring
* vh.textView.setTextFuture(future);
* }
* </pre>
* Because RecyclerView
* {@link androidx.recyclerview.widget.RecyclerView.LayoutManager#isItemPrefetchEnabled
* prefetches} bind multiple frames in advance while scrolling, the text work generally has
* plenty of time to complete before measurement occurs.
* </p>
* <p class="note">
* <strong>Note:</strong> all TextView layout properties must be set before creating the
* Params object. If they are changed during the precomputation, this can cause a
* {@link IllegalArgumentException} when the precomputed value is consumed during measure,
* and doesn't reflect the TextView's current state.
* </p>
* @param charSequence the text to be displayed
* @param params the parameters to be used for displaying text
* @param executor the executor to be process the text layout. If null is passed, the default
* single threaded pool will be used.
* @return a future of the precomputed text
*
* @see androidx.appcompat.widget.AppCompatTextView#setTextFuture
*/
@UiThread
public static Future<PrecomputedTextCompat> getTextFuture(
@NonNull final CharSequence charSequence, @NonNull PrecomputedTextCompat.Params params,
@Nullable Executor executor) {
PrecomputedTextFutureTask task = new PrecomputedTextFutureTask(params, charSequence);
if (executor == null) {
synchronized (sLock) {
if (sExecutor == null) {
sExecutor = Executors.newFixedThreadPool(1);
}
executor = sExecutor;
}
}
executor.execute(task);
return task;
}
///////////////////////////////////////////////////////////////////////////////////////////////
// Spannable overrides
//
// Do not allow to modify MetricAffectingSpan
/**
* @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified.
*/
@SuppressLint("NewApi") // TODO: Remove once Q SDK is released
@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.");
}
if (Build.VERSION.SDK_INT >= 29) {
mWrapped.setSpan(what, start, end, flags);
} else {
mText.setSpan(what, start, end, flags);
}
}
/**
* @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified.
*/
@SuppressLint("NewApi") // TODO: Remove once Q SDK is released
@Override
public void removeSpan(Object what) {
if (what instanceof MetricAffectingSpan) {
throw new IllegalArgumentException(
"MetricAffectingSpan can not be removed from PrecomputedText.");
}
if (Build.VERSION.SDK_INT >= 29) {
mWrapped.removeSpan(what);
} else {
mText.removeSpan(what);
}
}
///////////////////////////////////////////////////////////////////////////////////////////////
// Spanned overrides
//
// Just proxy for underlying mText if appropriate.
@SuppressLint("NewApi") // TODO: Remove once Q SDK is released
@Override
public <T> T[] getSpans(int start, int end, Class<T> type) {
if (Build.VERSION.SDK_INT >= 29) {
return mWrapped.getSpans(start, end, type);
} else {
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 mText.subSequence(start, end);
}
@NonNull
@Override
public String toString() {
return mText.toString();
}
}