Home | History | Annotate | Download | only in widget
      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;
     20 import static androidx.core.widget.AutoSizeableTextView.PLATFORM_SUPPORTS_AUTOSIZE;
     21 
     22 import android.annotation.SuppressLint;
     23 import android.content.Context;
     24 import android.content.res.ColorStateList;
     25 import android.content.res.Resources;
     26 import android.graphics.Typeface;
     27 import android.graphics.drawable.Drawable;
     28 import android.os.Build;
     29 import android.text.method.PasswordTransformationMethod;
     30 import android.util.AttributeSet;
     31 import android.util.TypedValue;
     32 import android.widget.TextView;
     33 
     34 import androidx.annotation.NonNull;
     35 import androidx.annotation.RestrictTo;
     36 import androidx.appcompat.R;
     37 import androidx.core.content.res.ResourcesCompat;
     38 import androidx.core.widget.TextViewCompat;
     39 
     40 import java.lang.ref.WeakReference;
     41 
     42 class AppCompatTextHelper {
     43 
     44     // Enum for the "typeface" XML parameter.
     45     private static final int SANS = 1;
     46     private static final int SERIF = 2;
     47     private static final int MONOSPACE = 3;
     48 
     49     private final TextView mView;
     50 
     51     private TintInfo mDrawableLeftTint;
     52     private TintInfo mDrawableTopTint;
     53     private TintInfo mDrawableRightTint;
     54     private TintInfo mDrawableBottomTint;
     55     private TintInfo mDrawableStartTint;
     56     private TintInfo mDrawableEndTint;
     57 
     58     private final @NonNull AppCompatTextViewAutoSizeHelper mAutoSizeTextHelper;
     59 
     60     private int mStyle = Typeface.NORMAL;
     61     private Typeface mFontTypeface;
     62     private boolean mAsyncFontPending;
     63 
     64     AppCompatTextHelper(TextView view) {
     65         mView = view;
     66         mAutoSizeTextHelper = new AppCompatTextViewAutoSizeHelper(mView);
     67     }
     68 
     69     @SuppressLint("NewApi")
     70     void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
     71         final Context context = mView.getContext();
     72         final AppCompatDrawableManager drawableManager = AppCompatDrawableManager.get();
     73 
     74         // First read the TextAppearance style id
     75         TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
     76                 R.styleable.AppCompatTextHelper, defStyleAttr, 0);
     77         final int ap = a.getResourceId(R.styleable.AppCompatTextHelper_android_textAppearance, -1);
     78         // Now read the compound drawable and grab any tints
     79         if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableLeft)) {
     80             mDrawableLeftTint = createTintInfo(context, drawableManager,
     81                     a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableLeft, 0));
     82         }
     83         if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableTop)) {
     84             mDrawableTopTint = createTintInfo(context, drawableManager,
     85                     a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableTop, 0));
     86         }
     87         if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableRight)) {
     88             mDrawableRightTint = createTintInfo(context, drawableManager,
     89                     a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableRight, 0));
     90         }
     91         if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableBottom)) {
     92             mDrawableBottomTint = createTintInfo(context, drawableManager,
     93                     a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableBottom, 0));
     94         }
     95 
     96         if (Build.VERSION.SDK_INT >= 17) {
     97             if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableStart)) {
     98                 mDrawableStartTint = createTintInfo(context, drawableManager,
     99                     a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableStart, 0));
    100             }
    101             if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableEnd)) {
    102                 mDrawableEndTint = createTintInfo(context, drawableManager,
    103                     a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableEnd, 0));
    104             }
    105         }
    106 
    107         a.recycle();
    108 
    109         // PasswordTransformationMethod wipes out all other TransformationMethod instances
    110         // in TextView's constructor, so we should only set a new transformation method
    111         // if we don't have a PasswordTransformationMethod currently...
    112         final boolean hasPwdTm =
    113                 mView.getTransformationMethod() instanceof PasswordTransformationMethod;
    114         boolean allCaps = false;
    115         boolean allCapsSet = false;
    116         ColorStateList textColor = null;
    117         ColorStateList textColorHint = null;
    118         ColorStateList textColorLink = null;
    119 
    120         // First check TextAppearance's textAllCaps value
    121         if (ap != -1) {
    122             a = TintTypedArray.obtainStyledAttributes(context, ap, R.styleable.TextAppearance);
    123             if (!hasPwdTm && a.hasValue(R.styleable.TextAppearance_textAllCaps)) {
    124                 allCapsSet = true;
    125                 allCaps = a.getBoolean(R.styleable.TextAppearance_textAllCaps, false);
    126             }
    127 
    128             updateTypefaceAndStyle(context, a);
    129             if (Build.VERSION.SDK_INT < 23) {
    130                 // If we're running on < API 23, the text color may contain theme references
    131                 // so let's re-set using our own inflater
    132                 if (a.hasValue(R.styleable.TextAppearance_android_textColor)) {
    133                     textColor = a.getColorStateList(R.styleable.TextAppearance_android_textColor);
    134                 }
    135                 if (a.hasValue(R.styleable.TextAppearance_android_textColorHint)) {
    136                     textColorHint = a.getColorStateList(
    137                             R.styleable.TextAppearance_android_textColorHint);
    138                 }
    139                 if (a.hasValue(R.styleable.TextAppearance_android_textColorLink)) {
    140                     textColorLink = a.getColorStateList(
    141                             R.styleable.TextAppearance_android_textColorLink);
    142                 }
    143             }
    144             a.recycle();
    145         }
    146 
    147         // Now read the style's values
    148         a = TintTypedArray.obtainStyledAttributes(context, attrs, R.styleable.TextAppearance,
    149                 defStyleAttr, 0);
    150         if (!hasPwdTm && a.hasValue(R.styleable.TextAppearance_textAllCaps)) {
    151             allCapsSet = true;
    152             allCaps = a.getBoolean(R.styleable.TextAppearance_textAllCaps, false);
    153         }
    154         if (Build.VERSION.SDK_INT < 23) {
    155             // If we're running on < API 23, the text color may contain theme references
    156             // so let's re-set using our own inflater
    157             if (a.hasValue(R.styleable.TextAppearance_android_textColor)) {
    158                 textColor = a.getColorStateList(R.styleable.TextAppearance_android_textColor);
    159             }
    160             if (a.hasValue(R.styleable.TextAppearance_android_textColorHint)) {
    161                 textColorHint = a.getColorStateList(
    162                         R.styleable.TextAppearance_android_textColorHint);
    163             }
    164             if (a.hasValue(R.styleable.TextAppearance_android_textColorLink)) {
    165                 textColorLink = a.getColorStateList(
    166                         R.styleable.TextAppearance_android_textColorLink);
    167             }
    168         }
    169 
    170         updateTypefaceAndStyle(context, a);
    171         a.recycle();
    172 
    173         if (textColor != null) {
    174             mView.setTextColor(textColor);
    175         }
    176         if (textColorHint != null) {
    177             mView.setHintTextColor(textColorHint);
    178         }
    179         if (textColorLink != null) {
    180             mView.setLinkTextColor(textColorLink);
    181         }
    182         if (!hasPwdTm && allCapsSet) {
    183             setAllCaps(allCaps);
    184         }
    185         if (mFontTypeface != null) {
    186             mView.setTypeface(mFontTypeface, mStyle);
    187         }
    188 
    189         mAutoSizeTextHelper.loadFromAttributes(attrs, defStyleAttr);
    190 
    191         if (PLATFORM_SUPPORTS_AUTOSIZE) {
    192             // Delegate auto-size functionality to the framework implementation.
    193             if (mAutoSizeTextHelper.getAutoSizeTextType()
    194                     != TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE) {
    195                 final int[] autoSizeTextSizesInPx =
    196                         mAutoSizeTextHelper.getAutoSizeTextAvailableSizes();
    197                 if (autoSizeTextSizesInPx.length > 0) {
    198                     if (mView.getAutoSizeStepGranularity() != AppCompatTextViewAutoSizeHelper
    199                             .UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) {
    200                         // Configured with granularity, preserve details.
    201                         mView.setAutoSizeTextTypeUniformWithConfiguration(
    202                                 mAutoSizeTextHelper.getAutoSizeMinTextSize(),
    203                                 mAutoSizeTextHelper.getAutoSizeMaxTextSize(),
    204                                 mAutoSizeTextHelper.getAutoSizeStepGranularity(),
    205                                 TypedValue.COMPLEX_UNIT_PX);
    206                     } else {
    207                         mView.setAutoSizeTextTypeUniformWithPresetSizes(
    208                                 autoSizeTextSizesInPx, TypedValue.COMPLEX_UNIT_PX);
    209                     }
    210                 }
    211             }
    212         }
    213 
    214         // Read line and baseline heights attributes.
    215         a = TintTypedArray.obtainStyledAttributes(context, attrs, R.styleable.AppCompatTextView);
    216         final int firstBaselineToTopHeight = a.getDimensionPixelSize(
    217                 R.styleable.AppCompatTextView_firstBaselineToTopHeight, -1);
    218         final int lastBaselineToBottomHeight = a.getDimensionPixelSize(
    219                 R.styleable.AppCompatTextView_lastBaselineToBottomHeight, -1);
    220         final int lineHeight = a.getDimensionPixelSize(
    221                 R.styleable.AppCompatTextView_lineHeight, -1);
    222         a.recycle();
    223         if (firstBaselineToTopHeight != -1) {
    224             TextViewCompat.setFirstBaselineToTopHeight(mView, firstBaselineToTopHeight);
    225         }
    226         if (lastBaselineToBottomHeight != -1) {
    227             TextViewCompat.setLastBaselineToBottomHeight(mView, lastBaselineToBottomHeight);
    228         }
    229         if (lineHeight != -1) {
    230             TextViewCompat.setLineHeight(mView, lineHeight);
    231         }
    232     }
    233 
    234     private void updateTypefaceAndStyle(Context context, TintTypedArray a) {
    235         mStyle = a.getInt(R.styleable.TextAppearance_android_textStyle, mStyle);
    236 
    237         if (a.hasValue(R.styleable.TextAppearance_android_fontFamily)
    238                 || a.hasValue(R.styleable.TextAppearance_fontFamily)) {
    239             mFontTypeface = null;
    240             int fontFamilyId = a.hasValue(R.styleable.TextAppearance_fontFamily)
    241                     ? R.styleable.TextAppearance_fontFamily
    242                     : R.styleable.TextAppearance_android_fontFamily;
    243             if (!context.isRestricted()) {
    244                 final WeakReference<TextView> textViewWeak = new WeakReference<>(mView);
    245                 ResourcesCompat.FontCallback replyCallback = new ResourcesCompat.FontCallback() {
    246                     @Override
    247                     public void onFontRetrieved(@NonNull Typeface typeface) {
    248                         onAsyncTypefaceReceived(textViewWeak, typeface);
    249                     }
    250 
    251                     @Override
    252                     public void onFontRetrievalFailed(int reason) {
    253                         // Do nothing.
    254                     }
    255                 };
    256                 try {
    257                     // Note the callback will be triggered on the UI thread.
    258                     mFontTypeface = a.getFont(fontFamilyId, mStyle, replyCallback);
    259                     // If this call gave us an immediate result, ignore any pending callbacks.
    260                     mAsyncFontPending = mFontTypeface == null;
    261                 } catch (UnsupportedOperationException | Resources.NotFoundException e) {
    262                     // Expected if it is not a font resource.
    263                 }
    264             }
    265             if (mFontTypeface == null) {
    266                 // Try with String. This is done by TextView JB+, but fails in ICS
    267                 String fontFamilyName = a.getString(fontFamilyId);
    268                 if (fontFamilyName != null) {
    269                     mFontTypeface = Typeface.create(fontFamilyName, mStyle);
    270                 }
    271             }
    272             return;
    273         }
    274 
    275         if (a.hasValue(R.styleable.TextAppearance_android_typeface)) {
    276             // Ignore previous pending fonts
    277             mAsyncFontPending = false;
    278             int typefaceIndex = a.getInt(R.styleable.TextAppearance_android_typeface, SANS);
    279             switch (typefaceIndex) {
    280                 case SANS:
    281                     mFontTypeface = Typeface.SANS_SERIF;
    282                     break;
    283 
    284                 case SERIF:
    285                     mFontTypeface = Typeface.SERIF;
    286                     break;
    287 
    288                 case MONOSPACE:
    289                     mFontTypeface = Typeface.MONOSPACE;
    290                     break;
    291             }
    292         }
    293     }
    294 
    295     private void onAsyncTypefaceReceived(WeakReference<TextView> textViewWeak, Typeface typeface) {
    296         if (mAsyncFontPending) {
    297             mFontTypeface = typeface;
    298             final TextView textView = textViewWeak.get();
    299             if (textView != null) {
    300                 textView.setTypeface(typeface, mStyle);
    301             }
    302         }
    303     }
    304 
    305     void onSetTextAppearance(Context context, int resId) {
    306         final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context,
    307                 resId, R.styleable.TextAppearance);
    308         if (a.hasValue(R.styleable.TextAppearance_textAllCaps)) {
    309             // This breaks away slightly from the logic in TextView.setTextAppearance that serves
    310             // as an "overlay" on the current state of the TextView. Since android:textAllCaps
    311             // may have been set to true in this text appearance, we need to make sure that
    312             // app:textAllCaps has the chance to override it
    313             setAllCaps(a.getBoolean(R.styleable.TextAppearance_textAllCaps, false));
    314         }
    315         if (Build.VERSION.SDK_INT < 23
    316                 && a.hasValue(R.styleable.TextAppearance_android_textColor)) {
    317             // If we're running on < API 23, the text color may contain theme references
    318             // so let's re-set using our own inflater
    319             final ColorStateList textColor
    320                     = a.getColorStateList(R.styleable.TextAppearance_android_textColor);
    321             if (textColor != null) {
    322                 mView.setTextColor(textColor);
    323             }
    324         }
    325 
    326         updateTypefaceAndStyle(context, a);
    327         a.recycle();
    328         if (mFontTypeface != null) {
    329             mView.setTypeface(mFontTypeface, mStyle);
    330         }
    331     }
    332 
    333     void setAllCaps(boolean allCaps) {
    334         mView.setAllCaps(allCaps);
    335     }
    336 
    337     void applyCompoundDrawablesTints() {
    338         if (mDrawableLeftTint != null || mDrawableTopTint != null ||
    339                 mDrawableRightTint != null || mDrawableBottomTint != null) {
    340             final Drawable[] compoundDrawables = mView.getCompoundDrawables();
    341             applyCompoundDrawableTint(compoundDrawables[0], mDrawableLeftTint);
    342             applyCompoundDrawableTint(compoundDrawables[1], mDrawableTopTint);
    343             applyCompoundDrawableTint(compoundDrawables[2], mDrawableRightTint);
    344             applyCompoundDrawableTint(compoundDrawables[3], mDrawableBottomTint);
    345         }
    346         if (Build.VERSION.SDK_INT >= 17) {
    347             if (mDrawableStartTint != null || mDrawableEndTint != null) {
    348                 final Drawable[] compoundDrawables = mView.getCompoundDrawablesRelative();
    349                 applyCompoundDrawableTint(compoundDrawables[0], mDrawableStartTint);
    350                 applyCompoundDrawableTint(compoundDrawables[2], mDrawableEndTint);
    351             }
    352         }
    353     }
    354 
    355     private void applyCompoundDrawableTint(Drawable drawable, TintInfo info) {
    356         if (drawable != null && info != null) {
    357             AppCompatDrawableManager.tintDrawable(drawable, info, mView.getDrawableState());
    358         }
    359     }
    360 
    361     private static TintInfo createTintInfo(Context context,
    362             AppCompatDrawableManager drawableManager, int drawableId) {
    363         final ColorStateList tintList = drawableManager.getTintList(context, drawableId);
    364         if (tintList != null) {
    365             final TintInfo tintInfo = new TintInfo();
    366             tintInfo.mHasTintList = true;
    367             tintInfo.mTintList = tintList;
    368             return tintInfo;
    369         }
    370         return null;
    371     }
    372 
    373     /** @hide */
    374     @RestrictTo(LIBRARY_GROUP)
    375     void onLayout(boolean changed, int left, int top, int right, int bottom) {
    376         if (!PLATFORM_SUPPORTS_AUTOSIZE) {
    377             autoSizeText();
    378         }
    379     }
    380 
    381     /** @hide */
    382     @RestrictTo(LIBRARY_GROUP)
    383     void setTextSize(int unit, float size) {
    384         if (!PLATFORM_SUPPORTS_AUTOSIZE) {
    385             if (!isAutoSizeEnabled()) {
    386                 setTextSizeInternal(unit, size);
    387             }
    388         }
    389     }
    390 
    391     /** @hide */
    392     @RestrictTo(LIBRARY_GROUP)
    393     void autoSizeText() {
    394         mAutoSizeTextHelper.autoSizeText();
    395     }
    396 
    397     /** @hide */
    398     @RestrictTo(LIBRARY_GROUP)
    399     boolean isAutoSizeEnabled() {
    400         return mAutoSizeTextHelper.isAutoSizeEnabled();
    401     }
    402 
    403     private void setTextSizeInternal(int unit, float size) {
    404         mAutoSizeTextHelper.setTextSizeInternal(unit, size);
    405     }
    406 
    407     void setAutoSizeTextTypeWithDefaults(@TextViewCompat.AutoSizeTextType int autoSizeTextType) {
    408         mAutoSizeTextHelper.setAutoSizeTextTypeWithDefaults(autoSizeTextType);
    409     }
    410 
    411     void setAutoSizeTextTypeUniformWithConfiguration(
    412             int autoSizeMinTextSize,
    413             int autoSizeMaxTextSize,
    414             int autoSizeStepGranularity,
    415             int unit) throws IllegalArgumentException {
    416         mAutoSizeTextHelper.setAutoSizeTextTypeUniformWithConfiguration(
    417                 autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit);
    418     }
    419 
    420     void setAutoSizeTextTypeUniformWithPresetSizes(@NonNull int[] presetSizes, int unit)
    421             throws IllegalArgumentException {
    422         mAutoSizeTextHelper.setAutoSizeTextTypeUniformWithPresetSizes(presetSizes, unit);
    423     }
    424 
    425     @TextViewCompat.AutoSizeTextType
    426     int getAutoSizeTextType() {
    427         return mAutoSizeTextHelper.getAutoSizeTextType();
    428     }
    429 
    430     int getAutoSizeStepGranularity() {
    431         return mAutoSizeTextHelper.getAutoSizeStepGranularity();
    432     }
    433 
    434     int getAutoSizeMinTextSize() {
    435         return mAutoSizeTextHelper.getAutoSizeMinTextSize();
    436     }
    437 
    438     int getAutoSizeMaxTextSize() {
    439         return mAutoSizeTextHelper.getAutoSizeMaxTextSize();
    440     }
    441 
    442     int[] getAutoSizeTextAvailableSizes() {
    443         return mAutoSizeTextHelper.getAutoSizeTextAvailableSizes();
    444     }
    445 }
    446