1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.appcompat.widget;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
20 import static androidx.appcompat.widget.ViewUtils.SDK_LEVEL_SUPPORTS_AUTOSIZE;
21 
22 import android.annotation.SuppressLint;
23 import android.content.Context;
24 import android.content.res.ColorStateList;
25 import android.graphics.PorterDuff;
26 import android.graphics.Typeface;
27 import android.graphics.drawable.Drawable;
28 import android.os.Build;
29 import android.os.Build.VERSION_CODES;
30 import android.text.InputFilter;
31 import android.util.AttributeSet;
32 import android.util.Log;
33 import android.view.ActionMode;
34 import android.view.inputmethod.EditorInfo;
35 import android.view.inputmethod.InputConnection;
36 import android.view.inputmethod.InputMethodManager;
37 import android.view.textclassifier.TextClassifier;
38 import android.widget.TextView;
39 
40 import androidx.annotation.DrawableRes;
41 import androidx.annotation.FloatRange;
42 import androidx.annotation.IntRange;
43 import androidx.annotation.Px;
44 import androidx.annotation.RequiresApi;
45 import androidx.annotation.RestrictTo;
46 import androidx.annotation.UiThread;
47 import androidx.appcompat.content.res.AppCompatResources;
48 import androidx.core.graphics.TypefaceCompat;
49 import androidx.core.text.PrecomputedTextCompat;
50 import androidx.core.view.TintableBackgroundView;
51 import androidx.core.widget.AutoSizeableTextView;
52 import androidx.core.widget.TextViewCompat;
53 import androidx.core.widget.TintableCompoundDrawablesView;
54 import androidx.resourceinspection.annotation.AppCompatShadowedAttributes;
55 
56 import org.jspecify.annotations.NonNull;
57 import org.jspecify.annotations.Nullable;
58 
59 import java.util.concurrent.ExecutionException;
60 import java.util.concurrent.Future;
61 
62 /**
63  * A {@link TextView} which supports compatible features on older versions of the platform,
64  * including:
65  * <ul>
66  * <li>Allows dynamic tint of its background via the background tint methods in
67  * {@link androidx.core.view.ViewCompat}.</li>
68  * <li>Allows setting of the background tint using
69  * {@link androidx.appcompat.R.attr#backgroundTint} and
70  * {@link androidx.appcompat.R.attr#backgroundTintMode}.</li>
71  * <li>Supports auto-sizing via {@link androidx.core.widget.TextViewCompat} by allowing to instruct
72  * a {@link TextView} to let the size of the text expand or contract automatically to fill its
73  * layout based on the TextView's characteristics and boundaries. The style attributes associated
74  * with auto-sizing are
75  * {@link androidx.appcompat.R.attr#autoSizeTextType},
76  * {@link androidx.appcompat.R.attr#autoSizeMinTextSize},
77  * {@link androidx.appcompat.R.attr#autoSizeMaxTextSize},
78  * {@link androidx.appcompat.R.attr#autoSizeStepGranularity} and
79  * {@link androidx.appcompat.R.attr#autoSizePresetSizes}, all of which work back to
80  * {@link VERSION_CODES#ICE_CREAM_SANDWICH Ice Cream Sandwich}.</li>
81  * </ul>
82  *
83  * <p>This will automatically be used when you use {@link TextView} in your layouts
84  * and the top-level activity / dialog is provided by
85  * <a href="{@docRoot}topic/libraries/support-library/packages.html#v7-appcompat">appcompat</a>.
86  * You should only need to manually use this class when writing custom views.</p>
87  */
88 @AppCompatShadowedAttributes
89 public class AppCompatTextView extends TextView implements TintableBackgroundView,
90         TintableCompoundDrawablesView, AutoSizeableTextView, EmojiCompatConfigurationView {
91 
92     private static final String TAG = "AppCompatTextView";
93     private final AppCompatBackgroundHelper mBackgroundTintHelper;
94     private final AppCompatTextHelper mTextHelper;
95     private final AppCompatTextClassifierHelper mTextClassifierHelper;
96     @SuppressWarnings("NotNullFieldNotInitialized") // initialized in getter
97     private @NonNull AppCompatEmojiTextHelper mEmojiTextViewHelper;
98 
99     private boolean mIsSetTypefaceProcessing = false;
100 
101     /**
102      * Equivalent to Typeface.mOriginalTypeface.
103      * Used to correctly emulate the behavior of getTypeface(), because we need to call setTypeface
104      * directly in order to implement caching of variation instances of typefaces.
105      */
106     private Typeface mOriginalTypeface;
107 
108     /**
109      * The last Typeface we are aware of being set on {@link #getPaint()}.
110      * Used to detect if it has been changed out from under us via directly calling
111      * {@link android.graphics.Paint#setTypeface(Typeface)} or
112      * {@link android.graphics.Paint#setFontVariationSettings(String)}
113      * (which is not supported, so this is a best-effort workaround).
114      *
115      * @see #setTypefaceInternal(Typeface)
116      */
117     private Typeface mLastKnownTypefaceSetOnPaint;
118 
119     /**
120      * The currently applied font variation settings.
121      * Used to make getFontVariationSettings somewhat more accurate with Typeface instance caching,
122      * as we don't call super.setFontVariationSettings.
123      */
124     private String mFontVariationSettings;
125 
126     private @Nullable SuperCaller mSuperCaller = null;
127 
128     private @Nullable Future<PrecomputedTextCompat> mPrecomputedTextFuture;
129 
AppCompatTextView(@onNull Context context)130     public AppCompatTextView(@NonNull Context context) {
131         this(context, null);
132     }
133 
AppCompatTextView(@onNull Context context, @Nullable AttributeSet attrs)134     public AppCompatTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
135         this(context, attrs, android.R.attr.textViewStyle);
136     }
137 
AppCompatTextView( @onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)138     public AppCompatTextView(
139             @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
140         super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
141 
142         ThemeUtils.checkAppCompatTheme(this, getContext());
143 
144         mBackgroundTintHelper = new AppCompatBackgroundHelper(this);
145         mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
146 
147         mTextHelper = new AppCompatTextHelper(this);
148         mTextHelper.loadFromAttributes(attrs, defStyleAttr);
149         mTextHelper.applyCompoundDrawablesTints();
150 
151         mTextClassifierHelper = new AppCompatTextClassifierHelper(this);
152 
153         AppCompatEmojiTextHelper emojiTextViewHelper = getEmojiTextViewHelper();
154         emojiTextViewHelper.loadFromAttributes(attrs, defStyleAttr);
155     }
156 
157     /**
158      * This may be called from super constructors.
159      */
getEmojiTextViewHelper()160     private @NonNull AppCompatEmojiTextHelper getEmojiTextViewHelper() {
161         //noinspection ConstantConditions
162         if (mEmojiTextViewHelper == null) {
163             mEmojiTextViewHelper = new AppCompatEmojiTextHelper(this);
164         }
165         return mEmojiTextViewHelper;
166     }
167 
168     @Override
setBackgroundResource(@rawableRes int resId)169     public void setBackgroundResource(@DrawableRes int resId) {
170         super.setBackgroundResource(resId);
171         if (mBackgroundTintHelper != null) {
172             mBackgroundTintHelper.onSetBackgroundResource(resId);
173         }
174     }
175 
176     @Override
setBackgroundDrawable(@ullable Drawable background)177     public void setBackgroundDrawable(@Nullable Drawable background) {
178         super.setBackgroundDrawable(background);
179         if (mBackgroundTintHelper != null) {
180             mBackgroundTintHelper.onSetBackgroundDrawable(background);
181         }
182     }
183 
184     /**
185      * This should be accessed via
186      * {@link androidx.core.view.ViewCompat#setBackgroundTintList(android.view.View, ColorStateList)}
187      */
188     @RestrictTo(LIBRARY_GROUP_PREFIX)
189     @Override
setSupportBackgroundTintList(@ullable ColorStateList tint)190     public void setSupportBackgroundTintList(@Nullable ColorStateList tint) {
191         if (mBackgroundTintHelper != null) {
192             mBackgroundTintHelper.setSupportBackgroundTintList(tint);
193         }
194     }
195 
196     /**
197      * This should be accessed via
198      * {@link androidx.core.view.ViewCompat#getBackgroundTintList(android.view.View)}
199      */
200     @RestrictTo(LIBRARY_GROUP_PREFIX)
201     @Override
getSupportBackgroundTintList()202     public @Nullable ColorStateList getSupportBackgroundTintList() {
203         return mBackgroundTintHelper != null
204                 ? mBackgroundTintHelper.getSupportBackgroundTintList() : null;
205     }
206 
207     /**
208      * This should be accessed via
209      * {@link androidx.core.view.ViewCompat#setBackgroundTintMode(android.view.View, PorterDuff.Mode)}
210      */
211     @RestrictTo(LIBRARY_GROUP_PREFIX)
212     @Override
setSupportBackgroundTintMode(PorterDuff.@ullable Mode tintMode)213     public void setSupportBackgroundTintMode(PorterDuff.@Nullable Mode tintMode) {
214         if (mBackgroundTintHelper != null) {
215             mBackgroundTintHelper.setSupportBackgroundTintMode(tintMode);
216         }
217     }
218 
219     /**
220      * This should be accessed via
221      * {@link androidx.core.view.ViewCompat#getBackgroundTintMode(android.view.View)}
222      */
223     @RestrictTo(LIBRARY_GROUP_PREFIX)
224     @Override
getSupportBackgroundTintMode()225     public PorterDuff.@Nullable Mode getSupportBackgroundTintMode() {
226         return mBackgroundTintHelper != null
227                 ? mBackgroundTintHelper.getSupportBackgroundTintMode() : null;
228     }
229 
230     @Override
setTextAppearance(Context context, int resId)231     public void setTextAppearance(Context context, int resId) {
232         super.setTextAppearance(context, resId);
233         if (mTextHelper != null) {
234             mTextHelper.onSetTextAppearance(context, resId);
235         }
236     }
237 
238     /**
239      * Set font variation settings.
240      * See {@link TextView#setFontVariationSettings(String)} for details.
241      * <p>
242      * <em>Note:</em> Due to performance optimizations,
243      * {@code getPaint().getFontVariationSettings()} will be less reliable than if not using
244      * AppCompatTextView.  You should prefer {@link #getFontVariationSettings()}, which will be more
245      * accurate. However, neither approach will work correctly if using Typeface objects with
246      * embedded font variation settings.
247      */
248     // Reference comparison with mLastKnownTypefaceSetOnPaint is intended;
249     // it should in fact be the exact instance, because we set it.
250     @SuppressWarnings("ReferenceEquality")
251     @RequiresApi(26)
252     @Override
setFontVariationSettings(@ullable String fontVariationSettings)253     public boolean setFontVariationSettings(@Nullable String fontVariationSettings) {
254         Typeface baseTypeface = mOriginalTypeface;
255         // Try to work around apps mutating the result of getPaint()
256         // See setTypefaceInternal doc comment for details.
257         if (mLastKnownTypefaceSetOnPaint != getPaint().getTypeface()) {
258             Log.w(TAG, "getPaint().getTypeface() changed unexpectedly."
259                     + " App code should not modify the result of getPaint().");
260             // Best effort: use that new Typeface instead.
261             baseTypeface = getPaint().getTypeface();
262         }
263         Typeface variationTypefaceInstance = AppCompatTextHelper.Api26Impl.createVariationInstance(
264                 baseTypeface, fontVariationSettings);
265         if (variationTypefaceInstance != null) {
266             setTypefaceInternal(variationTypefaceInstance);
267             mFontVariationSettings = fontVariationSettings;
268             return true;
269         } else {
270             return false;
271         }
272     }
273 
274     @RequiresApi(26)
275     @Override
getFontVariationSettings()276     public @Nullable String getFontVariationSettings() {
277         return mFontVariationSettings;
278     }
279 
280     @Override
setFilters(@uppressWarnings"ArrayReturn") InputFilter @onNull [] filters)281     public void setFilters(@SuppressWarnings("ArrayReturn") InputFilter @NonNull [] filters) {
282         super.setFilters(getEmojiTextViewHelper().getFilters(filters));
283     }
284 
285     @Override
setAllCaps(boolean allCaps)286     public void setAllCaps(boolean allCaps) {
287         super.setAllCaps(allCaps);
288         getEmojiTextViewHelper().setAllCaps(allCaps);
289     }
290 
291     @Override
setEmojiCompatEnabled(boolean enabled)292     public void setEmojiCompatEnabled(boolean enabled) {
293         getEmojiTextViewHelper().setEnabled(enabled);
294     }
295 
296     @Override
isEmojiCompatEnabled()297     public boolean isEmojiCompatEnabled() {
298         return getEmojiTextViewHelper().isEnabled();
299     }
300 
301     @Override
drawableStateChanged()302     protected void drawableStateChanged() {
303         super.drawableStateChanged();
304         if (mBackgroundTintHelper != null) {
305             mBackgroundTintHelper.applySupportBackgroundTint();
306         }
307         if (mTextHelper != null) {
308             mTextHelper.applyCompoundDrawablesTints();
309         }
310     }
311 
312     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)313     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
314         super.onLayout(changed, left, top, right, bottom);
315         if (mTextHelper != null) {
316             mTextHelper.onLayout(changed, left, top, right, bottom);
317         }
318     }
319 
320     @Override
setTextSize(int unit, float size)321     public void setTextSize(int unit, float size) {
322         if (SDK_LEVEL_SUPPORTS_AUTOSIZE) {
323             super.setTextSize(unit, size);
324         } else {
325             if (mTextHelper != null) {
326                 mTextHelper.setTextSize(unit, size);
327             }
328         }
329     }
330 
331     @Override
onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter)332     protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
333         super.onTextChanged(text, start, lengthBefore, lengthAfter);
334         boolean useHelper = mTextHelper != null && !SDK_LEVEL_SUPPORTS_AUTOSIZE
335                 && mTextHelper.isAutoSizeEnabled();
336         if (useHelper) {
337             mTextHelper.autoSizeText();
338         }
339     }
340 
341     /**
342      * This should be accessed via
343      * {@link androidx.core.widget.TextViewCompat#setAutoSizeTextTypeWithDefaults(
344      *TextView, int)}
345      *
346      */
347     @RestrictTo(LIBRARY_GROUP_PREFIX)
348     @Override
setAutoSizeTextTypeWithDefaults( @extViewCompat.AutoSizeTextType int autoSizeTextType)349     public void setAutoSizeTextTypeWithDefaults(
350             @TextViewCompat.AutoSizeTextType int autoSizeTextType) {
351         if (SDK_LEVEL_SUPPORTS_AUTOSIZE) {
352             getSuperCaller().setAutoSizeTextTypeWithDefaults(autoSizeTextType);
353         } else {
354             if (mTextHelper != null) {
355                 mTextHelper.setAutoSizeTextTypeWithDefaults(autoSizeTextType);
356             }
357         }
358     }
359 
360     /**
361      * This should be accessed via
362      * {@link androidx.core.widget.TextViewCompat#setAutoSizeTextTypeUniformWithConfiguration(
363      *TextView, int, int, int, int)}
364      *
365      */
366     @RestrictTo(LIBRARY_GROUP_PREFIX)
367     @Override
setAutoSizeTextTypeUniformWithConfiguration( int autoSizeMinTextSize, int autoSizeMaxTextSize, int autoSizeStepGranularity, int unit)368     public void setAutoSizeTextTypeUniformWithConfiguration(
369             int autoSizeMinTextSize,
370             int autoSizeMaxTextSize,
371             int autoSizeStepGranularity,
372             int unit) throws IllegalArgumentException {
373         if (SDK_LEVEL_SUPPORTS_AUTOSIZE) {
374             getSuperCaller().setAutoSizeTextTypeUniformWithConfiguration(autoSizeMinTextSize,
375                     autoSizeMaxTextSize, autoSizeStepGranularity, unit);
376         } else {
377             if (mTextHelper != null) {
378                 mTextHelper.setAutoSizeTextTypeUniformWithConfiguration(
379                         autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit);
380             }
381         }
382     }
383 
384     /**
385      * This should be accessed via
386      * {@link androidx.core.widget.TextViewCompat#setAutoSizeTextTypeUniformWithPresetSizes(
387      *TextView, int[], int)}
388      *
389      */
390     @RestrictTo(LIBRARY_GROUP_PREFIX)
391     @Override
setAutoSizeTextTypeUniformWithPresetSizes(int @NonNull [] presetSizes, int unit)392     public void setAutoSizeTextTypeUniformWithPresetSizes(int @NonNull [] presetSizes, int unit)
393             throws IllegalArgumentException {
394         if (SDK_LEVEL_SUPPORTS_AUTOSIZE) {
395             getSuperCaller().setAutoSizeTextTypeUniformWithPresetSizes(presetSizes, unit);
396         } else {
397             if (mTextHelper != null) {
398                 mTextHelper.setAutoSizeTextTypeUniformWithPresetSizes(presetSizes, unit);
399             }
400         }
401     }
402 
403     /**
404      * This should be accessed via
405      * {@link androidx.core.widget.TextViewCompat#getAutoSizeTextType(TextView)}
406      *
407      */
408     @RestrictTo(LIBRARY_GROUP_PREFIX)
409     @Override
410     @TextViewCompat.AutoSizeTextType
411     // Suppress lint error for TextView.AUTO_SIZE_TEXT_TYPE_UNIFORM [WrongConstant]
412     @SuppressLint("WrongConstant")
getAutoSizeTextType()413     public int getAutoSizeTextType() {
414         if (SDK_LEVEL_SUPPORTS_AUTOSIZE) {
415             return getSuperCaller().getAutoSizeTextType() == TextView
416                     .AUTO_SIZE_TEXT_TYPE_UNIFORM
417                     ? TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM
418                     : TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE;
419         } else {
420             if (mTextHelper != null) {
421                 return mTextHelper.getAutoSizeTextType();
422             }
423         }
424         return TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE;
425     }
426 
427     /**
428      * This should be accessed via
429      * {@link androidx.core.widget.TextViewCompat#getAutoSizeStepGranularity(TextView)}
430      *
431      */
432     @RestrictTo(LIBRARY_GROUP_PREFIX)
433     @Override
getAutoSizeStepGranularity()434     public int getAutoSizeStepGranularity() {
435         if (SDK_LEVEL_SUPPORTS_AUTOSIZE) {
436             return getSuperCaller().getAutoSizeStepGranularity();
437         } else {
438             if (mTextHelper != null) {
439                 return mTextHelper.getAutoSizeStepGranularity();
440             }
441         }
442         return -1;
443     }
444 
445     /**
446      * This should be accessed via
447      * {@link androidx.core.widget.TextViewCompat#getAutoSizeMinTextSize(TextView)}
448      *
449      */
450     @RestrictTo(LIBRARY_GROUP_PREFIX)
451     @Override
getAutoSizeMinTextSize()452     public int getAutoSizeMinTextSize() {
453         if (SDK_LEVEL_SUPPORTS_AUTOSIZE) {
454             return getSuperCaller().getAutoSizeMinTextSize();
455         } else {
456             if (mTextHelper != null) {
457                 return mTextHelper.getAutoSizeMinTextSize();
458             }
459         }
460         return -1;
461     }
462 
463     /**
464      * This should be accessed via
465      * {@link androidx.core.widget.TextViewCompat#getAutoSizeMaxTextSize(TextView)}
466      *
467      */
468     @RestrictTo(LIBRARY_GROUP_PREFIX)
469     @Override
getAutoSizeMaxTextSize()470     public int getAutoSizeMaxTextSize() {
471         if (SDK_LEVEL_SUPPORTS_AUTOSIZE) {
472             return getSuperCaller().getAutoSizeMaxTextSize();
473         } else {
474             if (mTextHelper != null) {
475                 return mTextHelper.getAutoSizeMaxTextSize();
476             }
477         }
478         return -1;
479     }
480 
481     /**
482      * This should be accessed via
483      * {@link androidx.core.widget.TextViewCompat#getAutoSizeTextAvailableSizes(TextView)}
484      *
485      */
486     @RestrictTo(LIBRARY_GROUP_PREFIX)
487     @Override
getAutoSizeTextAvailableSizes()488     public int[] getAutoSizeTextAvailableSizes() {
489         if (SDK_LEVEL_SUPPORTS_AUTOSIZE) {
490             return getSuperCaller().getAutoSizeTextAvailableSizes();
491         } else {
492             if (mTextHelper != null) {
493                 return mTextHelper.getAutoSizeTextAvailableSizes();
494             }
495         }
496         return new int[0];
497     }
498 
499     @Override
onCreateInputConnection(EditorInfo outAttrs)500     public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
501         InputConnection ic = super.onCreateInputConnection(outAttrs);
502         mTextHelper.populateSurroundingTextIfNeeded(this, ic, outAttrs);
503         return AppCompatHintHelper.onCreateInputConnection(ic, outAttrs, this);
504     }
505 
506     @Override
setFirstBaselineToTopHeight(@x @ntRangefrom = 0) int firstBaselineToTopHeight)507     public void setFirstBaselineToTopHeight(@Px @IntRange(from = 0) int firstBaselineToTopHeight) {
508         if (Build.VERSION.SDK_INT >= 28) {
509             getSuperCaller().setFirstBaselineToTopHeight(firstBaselineToTopHeight);
510         } else {
511             TextViewCompat.setFirstBaselineToTopHeight(this, firstBaselineToTopHeight);
512         }
513     }
514 
515     @Override
setLastBaselineToBottomHeight( @x @ntRangefrom = 0) int lastBaselineToBottomHeight)516     public void setLastBaselineToBottomHeight(
517             @Px @IntRange(from = 0) int lastBaselineToBottomHeight) {
518         if (Build.VERSION.SDK_INT >= 28) {
519             getSuperCaller().setLastBaselineToBottomHeight(lastBaselineToBottomHeight);
520         } else {
521             TextViewCompat.setLastBaselineToBottomHeight(this,
522                     lastBaselineToBottomHeight);
523         }
524     }
525 
526     @Override
getFirstBaselineToTopHeight()527     public int getFirstBaselineToTopHeight() {
528         return TextViewCompat.getFirstBaselineToTopHeight(this);
529     }
530 
531     @Override
getLastBaselineToBottomHeight()532     public int getLastBaselineToBottomHeight() {
533         return TextViewCompat.getLastBaselineToBottomHeight(this);
534     }
535 
536     @Override
setLineHeight(@x @ntRangefrom = 0) int lineHeight)537     public void setLineHeight(@Px @IntRange(from = 0) int lineHeight) {
538         TextViewCompat.setLineHeight(this, lineHeight);
539     }
540 
541     @Override
setLineHeight(int unit, @FloatRange(from = 0) float lineHeight)542     public void setLineHeight(int unit, @FloatRange(from = 0) float lineHeight) {
543         if (Build.VERSION.SDK_INT >= 34) {
544             getSuperCaller().setLineHeight(unit, lineHeight);
545         } else {
546             TextViewCompat.setLineHeight(this, unit, lineHeight);
547         }
548     }
549 
550     /**
551      * See
552      * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
553      */
554     @Override
setCustomSelectionActionModeCallback( ActionMode.@ullable Callback actionModeCallback)555     public void setCustomSelectionActionModeCallback(
556             ActionMode.@Nullable Callback actionModeCallback) {
557         super.setCustomSelectionActionModeCallback(
558                 TextViewCompat.wrapCustomSelectionActionModeCallback(this, actionModeCallback));
559     }
560 
561     @Override
getCustomSelectionActionModeCallback()562     public ActionMode.@Nullable Callback getCustomSelectionActionModeCallback() {
563         return TextViewCompat.unwrapCustomSelectionActionModeCallback(
564                 super.getCustomSelectionActionModeCallback());
565     }
566 
567     /**
568      * Gets the parameters for text layout precomputation, for use with
569      * {@link PrecomputedTextCompat}.
570      *
571      * @return a current {@link PrecomputedTextCompat.Params}
572      * @see PrecomputedTextCompat
573      */
getTextMetricsParamsCompat()574     public PrecomputedTextCompat.@NonNull Params getTextMetricsParamsCompat() {
575         return TextViewCompat.getTextMetricsParams(this);
576     }
577 
578     /**
579      * Apply the text layout parameter.
580      *
581      * Update the TextView parameters to be compatible with {@link PrecomputedTextCompat.Params}.
582      *
583      * @see PrecomputedTextCompat
584      */
setTextMetricsParamsCompat(PrecomputedTextCompat.@onNull Params params)585     public void setTextMetricsParamsCompat(PrecomputedTextCompat.@NonNull Params params) {
586         TextViewCompat.setTextMetricsParams(this, params);
587     }
588 
589     /**
590      * Sets the PrecomputedTextCompat to the TextView.
591      *
592      * If the given PrecomputeTextCompat is not compatible with textView, throws an
593      * IllegalArgumentException.
594      *
595      * @param precomputed the precomputed text
596      * @throws IllegalArgumentException if precomputed text is not compatible with textView.
597      */
setPrecomputedText(@onNull PrecomputedTextCompat precomputed)598     public void setPrecomputedText(@NonNull PrecomputedTextCompat precomputed) {
599         TextViewCompat.setPrecomputedText(this, precomputed);
600     }
601 
consumeTextFutureAndSetBlocking()602     private void consumeTextFutureAndSetBlocking() {
603         if (mPrecomputedTextFuture != null) {
604             try {
605                 Future<PrecomputedTextCompat> future = mPrecomputedTextFuture;
606                 mPrecomputedTextFuture = null;
607                 TextViewCompat.setPrecomputedText(this, future.get());
608             } catch (InterruptedException | ExecutionException e) {
609                 // ignore
610             }
611         }
612     }
613 
614     @Override
getText()615     public CharSequence getText() {
616         consumeTextFutureAndSetBlocking();
617         return super.getText();
618     }
619 
620     /**
621      * Sets the {@link TextClassifier} for this TextView.
622      */
623     @Override
624     @RequiresApi(api = 26)
setTextClassifier(@ullable TextClassifier textClassifier)625     public void setTextClassifier(@Nullable TextClassifier textClassifier) {
626         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P || mTextClassifierHelper == null) {
627             getSuperCaller().setTextClassifier(textClassifier);
628             return;
629         }
630         mTextClassifierHelper.setTextClassifier(textClassifier);
631     }
632 
633     /**
634      * Returns the {@link TextClassifier} used by this TextView.
635      * If no TextClassifier has been set, this TextView uses the default set by the
636      * {@link android.view.textclassifier.TextClassificationManager}.
637      */
638     @Override
639     @RequiresApi(api = 26)
getTextClassifier()640     public @NonNull TextClassifier getTextClassifier() {
641         // The null check is necessary because getTextClassifier is called when we are invoking
642         // the super class's constructor.
643         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P || mTextClassifierHelper == null) {
644             return getSuperCaller().getTextClassifier();
645         }
646         return mTextClassifierHelper.getTextClassifier();
647     }
648 
649     /**
650      * Set the precomputed text future.
651      *
652      * This method sets future of the precomputed text instead of immediately applying text to the
653      * TextView. Anything layout related property changes, text size, typeface, letter spacing, etc
654      * after this method call will causes IllegalArgumentException during View measurement.
655      *
656      * See {@link PrecomputedTextCompat#getTextFuture} for more detail.
657      *
658      * @param future a future for the precomputed text
659      * @see PrecomputedTextCompat#getTextFuture
660      */
setTextFuture(@ullable Future<PrecomputedTextCompat> future)661     public void setTextFuture(@Nullable Future<PrecomputedTextCompat> future) {
662         mPrecomputedTextFuture = future;
663         if (future != null) {
664             requestLayout();
665         }
666     }
667 
668     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)669     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
670         consumeTextFutureAndSetBlocking();
671         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
672     }
673 
674     @Override
setCompoundDrawables(@ullable Drawable left, @Nullable Drawable top, @Nullable Drawable right, @Nullable Drawable bottom)675     public void setCompoundDrawables(@Nullable Drawable left, @Nullable Drawable top,
676             @Nullable Drawable right, @Nullable Drawable bottom) {
677         super.setCompoundDrawables(left, top, right, bottom);
678         if (mTextHelper != null) {
679             mTextHelper.onSetCompoundDrawables();
680         }
681     }
682 
683     @Override
setCompoundDrawablesRelative(@ullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, @Nullable Drawable bottom)684     public void setCompoundDrawablesRelative(@Nullable Drawable start, @Nullable Drawable top,
685             @Nullable Drawable end, @Nullable Drawable bottom) {
686         super.setCompoundDrawablesRelative(start, top, end, bottom);
687         if (mTextHelper != null) {
688             mTextHelper.onSetCompoundDrawables();
689         }
690     }
691 
692     @Override
setCompoundDrawablesWithIntrinsicBounds(@ullable Drawable left, @Nullable Drawable top, @Nullable Drawable right, @Nullable Drawable bottom)693     public void setCompoundDrawablesWithIntrinsicBounds(@Nullable Drawable left,
694             @Nullable Drawable top, @Nullable Drawable right, @Nullable Drawable bottom) {
695         super.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
696         if (mTextHelper != null) {
697             mTextHelper.onSetCompoundDrawables();
698         }
699     }
700 
701     @Override
setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom)702     public void setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom) {
703         final Context context = getContext();
704         setCompoundDrawablesWithIntrinsicBounds(
705                 left != 0 ? AppCompatResources.getDrawable(context, left) : null,
706                 top != 0 ? AppCompatResources.getDrawable(context, top) : null,
707                 right != 0 ? AppCompatResources.getDrawable(context, right) : null,
708                 bottom != 0 ? AppCompatResources.getDrawable(context, bottom) : null);
709         if (mTextHelper != null) {
710             mTextHelper.onSetCompoundDrawables();
711         }
712     }
713 
714     @Override
setCompoundDrawablesRelativeWithIntrinsicBounds(@ullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, @Nullable Drawable bottom)715     public void setCompoundDrawablesRelativeWithIntrinsicBounds(@Nullable Drawable start,
716             @Nullable Drawable top, @Nullable Drawable end, @Nullable Drawable bottom) {
717         super.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
718         if (mTextHelper != null) {
719             mTextHelper.onSetCompoundDrawables();
720         }
721     }
722 
723     @Override
setCompoundDrawablesRelativeWithIntrinsicBounds( int start, int top, int end, int bottom)724     public void setCompoundDrawablesRelativeWithIntrinsicBounds(
725             int start, int top, int end, int bottom) {
726         final Context context = getContext();
727         setCompoundDrawablesRelativeWithIntrinsicBounds(
728                 start != 0 ? AppCompatResources.getDrawable(context, start) : null,
729                 top != 0 ? AppCompatResources.getDrawable(context, top) : null,
730                 end != 0 ? AppCompatResources.getDrawable(context, end) : null,
731                 bottom != 0 ? AppCompatResources.getDrawable(context, bottom) : null);
732         if (mTextHelper != null) {
733             mTextHelper.onSetCompoundDrawables();
734         }
735     }
736 
737     /**
738      * This should be accessed via
739      * {@link androidx.core.widget.TextViewCompat#getCompoundDrawableTintList(TextView)}
740      *
741      * @return the tint applied to the compound drawables
742      * @attr ref androidx.appcompat.R.styleable#AppCompatTextView_drawableTint
743      * @see #setSupportCompoundDrawablesTintList(ColorStateList)
744      *
745      */
746     @Override
747     @RestrictTo(LIBRARY_GROUP_PREFIX)
getSupportCompoundDrawablesTintList()748     public @Nullable ColorStateList getSupportCompoundDrawablesTintList() {
749         return mTextHelper.getCompoundDrawableTintList();
750     }
751 
752     /**
753      * This should be accessed via {@link
754      * androidx.core.widget.TextViewCompat#setCompoundDrawableTintList(TextView, ColorStateList)}
755      *
756      * Applies a tint to the compound drawables. Does not modify the current tint mode, which is
757      * {@link PorterDuff.Mode#SRC_IN} by default.
758      * <p>
759      * Subsequent calls to {@link #setCompoundDrawables(Drawable, Drawable, Drawable, Drawable)} and
760      * related methods will automatically mutate the drawables and apply the specified tint and tint
761      * mode using {@link Drawable#setTintList(ColorStateList)}.
762      *
763      * @param tintList the tint to apply, may be {@code null} to clear tint
764      * @attr ref androidx.appcompat.R.styleable#AppCompatTextView_drawableTint
765      * @see #getSupportCompoundDrawablesTintList()
766      *
767      */
768     @Override
769     @RestrictTo(LIBRARY_GROUP_PREFIX)
setSupportCompoundDrawablesTintList(@ullable ColorStateList tintList)770     public void setSupportCompoundDrawablesTintList(@Nullable ColorStateList tintList) {
771         mTextHelper.setCompoundDrawableTintList(tintList);
772         mTextHelper.applyCompoundDrawablesTints();
773     }
774 
775     /**
776      * This should be accessed via
777      * {@link androidx.core.widget.TextViewCompat#getCompoundDrawableTintMode(TextView)}
778      *
779      * Returns the blending mode used to apply the tint to the compound drawables, if specified.
780      *
781      * @return the blending mode used to apply the tint to the compound drawables
782      * @attr ref androidx.appcompat.R.styleable#AppCompatTextView_drawableTintMode
783      * @see #setSupportCompoundDrawablesTintMode(PorterDuff.Mode)
784      *
785      */
786     @Override
787     @RestrictTo(LIBRARY_GROUP_PREFIX)
getSupportCompoundDrawablesTintMode()788     public PorterDuff.@Nullable Mode getSupportCompoundDrawablesTintMode() {
789         return mTextHelper.getCompoundDrawableTintMode();
790     }
791 
792     /**
793      * This should be accessed via {@link
794      * androidx.core.widget.TextViewCompat#setCompoundDrawableTintMode(TextView, PorterDuff.Mode)}
795      *
796      * Specifies the blending mode used to apply the tint specified by
797      * {@link #setSupportCompoundDrawablesTintList(ColorStateList)} to the compound drawables. The
798      * default mode is {@link PorterDuff.Mode#SRC_IN}.
799      *
800      * @param tintMode the blending mode used to apply the tint, may be {@code null} to clear tint
801      * @attr ref androidx.appcompat.R.styleable#AppCompatTextView_drawableTintMode
802      * @see #setSupportCompoundDrawablesTintList(ColorStateList)
803      *
804      */
805     @Override
806     @RestrictTo(LIBRARY_GROUP_PREFIX)
setSupportCompoundDrawablesTintMode(PorterDuff.@ullable Mode tintMode)807     public void setSupportCompoundDrawablesTintMode(PorterDuff.@Nullable Mode tintMode) {
808         mTextHelper.setCompoundDrawableTintMode(tintMode);
809         mTextHelper.applyCompoundDrawablesTints();
810     }
811 
812     // Never call super.setTypeface directly, always use this or setTypefaceInternal
813     // See docs on setTypefaceInternal for the differences
814     @Override
setTypeface(@ullable Typeface tf)815     public void setTypeface(@Nullable Typeface tf) {
816         mOriginalTypeface = tf;
817         setTypefaceInternal(tf);
818     }
819 
820     /**
821      * Call this when setting the typeface in any way that the user didn't directly ask for
822      * (that is, any case where TextView itself does not call through to setTypeface or otherwise
823      * set its mOriginalTypeface).  Otherwise, use {@link #setTypeface(Typeface)} (or something
824      * that calls it).
825      * <p>
826      * Calls the superclass setTypeface, but does not set mOriginalTypeface.
827      * Also tracks what we set it to, in order to detect when it's been changed out from under us
828      * via modifying the Paint object directly.
829      * This isn't officially supported ({@link TextView#getPaint()} specifically says not to modify
830      * it), but at least one app is known to have done this, so we're providing best-effort support.
831      */
setTypefaceInternal(@ullable Typeface tf)832     private void setTypefaceInternal(@Nullable Typeface tf) {
833         mLastKnownTypefaceSetOnPaint = tf;
834         super.setTypeface(tf);
835     }
836 
837     @Override
838     // Code inspection reveals that the superclass method can return null.
839     @SuppressWarnings("InvalidNullabilityOverride")
getTypeface()840     public @Nullable Typeface getTypeface() {
841         return mOriginalTypeface;
842     }
843 
844     @Override
setTypeface(@ullable Typeface tf, int style)845     public void setTypeface(@Nullable Typeface tf, int style) {
846         if (mIsSetTypefaceProcessing) {
847             // b/151782655
848             // Some device up to API19 recursively calls setTypeface. To avoid infinity recursive
849             // setTypeface call, exit if we know this is re-entrant call.
850             // TODO(nona): Remove this once Android X minSdkVersion moves to API21.
851             return;
852         }
853         final Typeface finalTypeface;
854         if (tf != null && style > 0) {
855             finalTypeface = TypefaceCompat.create(getContext(), tf, style);
856         } else {
857             finalTypeface = tf;
858         }
859 
860         mIsSetTypefaceProcessing = true;
861         try {
862             super.setTypeface(finalTypeface, style);
863         } finally {
864             mIsSetTypefaceProcessing = false;
865         }
866 
867     }
868 
869     @Override
onDetachedFromWindow()870     protected void onDetachedFromWindow() {
871         super.onDetachedFromWindow();
872         if (Build.VERSION.SDK_INT >= 30 && Build.VERSION.SDK_INT < 33 && onCheckIsTextEditor()) {
873             final InputMethodManager imm = (InputMethodManager) getContext().getSystemService(
874                     Context.INPUT_METHOD_SERVICE);
875             // If the AppCompatTextView is editable, user can input text on it.
876             // Calling isActive() here implied a checkFocus() call to update the active served
877             // view for input method. This is a backport for mServedView was detached, but the
878             // next served view gets mistakenly cleared as well.
879             // https://android.googlesource.com/platform/frameworks/base/+/734613a500fb
880             imm.isActive(this);
881         }
882     }
883 
884     @UiThread
885     @RequiresApi(api = 26)
getSuperCaller()886     SuperCaller getSuperCaller() {
887         if (mSuperCaller == null) {
888             if (Build.VERSION.SDK_INT >= 34) {
889                 mSuperCaller = new SuperCallerApi34();
890             } else if (Build.VERSION.SDK_INT >= 28) {
891                 mSuperCaller = new SuperCallerApi28();
892             } else if (Build.VERSION.SDK_INT >= 26) {
893                 mSuperCaller = new SuperCallerApi26();
894             }
895         }
896         return mSuperCaller;
897     }
898 
899 
900 
901     private interface SuperCaller {
902         // api 26
getAutoSizeMaxTextSize()903         int getAutoSizeMaxTextSize();
getAutoSizeMinTextSize()904         int getAutoSizeMinTextSize();
getAutoSizeStepGranularity()905         int getAutoSizeStepGranularity();
getAutoSizeTextAvailableSizes()906         int[] getAutoSizeTextAvailableSizes();
getAutoSizeTextType()907         int getAutoSizeTextType();
getTextClassifier()908         TextClassifier getTextClassifier();
setAutoSizeTextTypeUniformWithConfiguration(int autoSizeMinTextSize, int autoSizeMaxTextSize, int autoSizeStepGranularity, int unit)909         void setAutoSizeTextTypeUniformWithConfiguration(int autoSizeMinTextSize,
910                 int autoSizeMaxTextSize, int autoSizeStepGranularity, int unit);
setAutoSizeTextTypeUniformWithPresetSizes(int[] presetSizes, int unit)911         void setAutoSizeTextTypeUniformWithPresetSizes(int[] presetSizes, int unit);
setAutoSizeTextTypeWithDefaults(int autoSizeTextType)912         void setAutoSizeTextTypeWithDefaults(int autoSizeTextType);
setTextClassifier(@ullable TextClassifier textClassifier)913         void setTextClassifier(@Nullable TextClassifier textClassifier);
914 
915         // api 28
setFirstBaselineToTopHeight(@x int firstBaselineToTopHeight)916         void setFirstBaselineToTopHeight(@Px int firstBaselineToTopHeight);
setLastBaselineToBottomHeight(@x int lastBaselineToBottomHeight)917         void setLastBaselineToBottomHeight(@Px int lastBaselineToBottomHeight);
918 
919         // api 34
setLineHeight(int unit, @FloatRange(from = 0) float lineHeight)920         void setLineHeight(int unit, @FloatRange(from = 0) float lineHeight);
921     }
922 
923     @RequiresApi(api = 26)
924     class SuperCallerApi26 implements SuperCaller {
925         @Override
getAutoSizeMaxTextSize()926         public int getAutoSizeMaxTextSize() {
927             return AppCompatTextView.super.getAutoSizeMaxTextSize();
928         }
929 
930         @Override
getAutoSizeMinTextSize()931         public int getAutoSizeMinTextSize() {
932             return AppCompatTextView.super.getAutoSizeMinTextSize();
933         }
934 
935         @Override
getAutoSizeStepGranularity()936         public int getAutoSizeStepGranularity() {
937             return AppCompatTextView.super.getAutoSizeStepGranularity();
938         }
939 
940         @Override
getAutoSizeTextAvailableSizes()941         public int[] getAutoSizeTextAvailableSizes() {
942             return AppCompatTextView.super.getAutoSizeTextAvailableSizes();
943         }
944 
945         @Override
getAutoSizeTextType()946         public int getAutoSizeTextType() {
947             return AppCompatTextView.super.getAutoSizeTextType();
948         }
949 
950         @Override
getTextClassifier()951         public TextClassifier getTextClassifier() {
952             return AppCompatTextView.super.getTextClassifier();
953         }
954 
955         @Override
setAutoSizeTextTypeUniformWithConfiguration(int autoSizeMinTextSize, int autoSizeMaxTextSize, int autoSizeStepGranularity, int unit)956         public void setAutoSizeTextTypeUniformWithConfiguration(int autoSizeMinTextSize,
957                 int autoSizeMaxTextSize, int autoSizeStepGranularity, int unit) {
958             AppCompatTextView.super.setAutoSizeTextTypeUniformWithConfiguration(autoSizeMinTextSize,
959                     autoSizeMaxTextSize, autoSizeStepGranularity, unit);
960         }
961 
962         @Override
setAutoSizeTextTypeUniformWithPresetSizes(int[] presetSizes, int unit)963         public void setAutoSizeTextTypeUniformWithPresetSizes(int[] presetSizes, int unit) {
964             AppCompatTextView.super.setAutoSizeTextTypeUniformWithPresetSizes(presetSizes, unit);
965         }
966 
967         @Override
setAutoSizeTextTypeWithDefaults(int autoSizeTextType)968         public void setAutoSizeTextTypeWithDefaults(int autoSizeTextType) {
969             AppCompatTextView.super.setAutoSizeTextTypeWithDefaults(autoSizeTextType);
970         }
971 
972         @Override
setTextClassifier(@ullable TextClassifier textClassifier)973         public void setTextClassifier(@Nullable TextClassifier textClassifier) {
974             AppCompatTextView.super.setTextClassifier(textClassifier);
975         }
976 
977         @Override
setFirstBaselineToTopHeight(int firstBaselineToTopHeight)978         public void setFirstBaselineToTopHeight(int firstBaselineToTopHeight) {}
979 
980         @Override
setLastBaselineToBottomHeight(int lastBaselineToBottomHeight)981         public void setLastBaselineToBottomHeight(int lastBaselineToBottomHeight) {}
982 
983         @Override
setLineHeight(int unit, float lineHeight)984         public void setLineHeight(int unit, float lineHeight) {}
985     }
986 
987     @RequiresApi(api = 28)
988     class SuperCallerApi28 extends SuperCallerApi26 {
989 
990         @Override
setFirstBaselineToTopHeight(@x int firstBaselineToTopHeight)991         public void setFirstBaselineToTopHeight(@Px int firstBaselineToTopHeight) {
992             AppCompatTextView.super.setFirstBaselineToTopHeight(firstBaselineToTopHeight);
993         }
994 
995         @Override
setLastBaselineToBottomHeight(@x int lastBaselineToBottomHeight)996         public void setLastBaselineToBottomHeight(@Px int lastBaselineToBottomHeight) {
997             AppCompatTextView.super.setLastBaselineToBottomHeight(lastBaselineToBottomHeight);
998         }
999     }
1000 
1001     @RequiresApi(api = 34)
1002     class SuperCallerApi34 extends SuperCallerApi28 {
1003         @Override
setLineHeight(int unit, float lineHeight)1004         public void setLineHeight(int unit, float lineHeight) {
1005             AppCompatTextView.super.setLineHeight(unit, lineHeight);
1006         }
1007     }
1008 }
1009