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 android.support.v7.widget;
18 
19 import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20 
21 import android.content.Context;
22 import android.content.res.ColorStateList;
23 import android.content.res.Resources;
24 import android.graphics.Typeface;
25 import android.graphics.drawable.Drawable;
26 import android.os.Build;
27 import android.support.annotation.NonNull;
28 import android.support.annotation.RequiresApi;
29 import android.support.annotation.RestrictTo;
30 import android.support.v4.os.BuildCompat;
31 import android.support.v4.widget.TextViewCompat;
32 import android.support.v7.appcompat.R;
33 import android.text.method.PasswordTransformationMethod;
34 import android.util.AttributeSet;
35 import android.util.TypedValue;
36 import android.widget.TextView;
37 
38 @RequiresApi(9)
39 class AppCompatTextHelper {
40 
create(TextView textView)41     static AppCompatTextHelper create(TextView textView) {
42         if (Build.VERSION.SDK_INT >= 17) {
43             return new AppCompatTextHelperV17(textView);
44         }
45         return new AppCompatTextHelper(textView);
46     }
47 
48     final TextView mView;
49 
50     private TintInfo mDrawableLeftTint;
51     private TintInfo mDrawableTopTint;
52     private TintInfo mDrawableRightTint;
53     private TintInfo mDrawableBottomTint;
54 
55     private final @NonNull AppCompatTextViewAutoSizeHelper mAutoSizeTextHelper;
56 
57     private int mStyle = Typeface.NORMAL;
58     private Typeface mFontTypeface;
59 
AppCompatTextHelper(TextView view)60     AppCompatTextHelper(TextView view) {
61         mView = view;
62         mAutoSizeTextHelper = new AppCompatTextViewAutoSizeHelper(mView);
63     }
64 
loadFromAttributes(AttributeSet attrs, int defStyleAttr)65     void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
66         final Context context = mView.getContext();
67         final AppCompatDrawableManager drawableManager = AppCompatDrawableManager.get();
68 
69         // First read the TextAppearance style id
70         TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
71                 R.styleable.AppCompatTextHelper, defStyleAttr, 0);
72         final int ap = a.getResourceId(R.styleable.AppCompatTextHelper_android_textAppearance, -1);
73         // Now read the compound drawable and grab any tints
74         if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableLeft)) {
75             mDrawableLeftTint = createTintInfo(context, drawableManager,
76                     a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableLeft, 0));
77         }
78         if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableTop)) {
79             mDrawableTopTint = createTintInfo(context, drawableManager,
80                     a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableTop, 0));
81         }
82         if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableRight)) {
83             mDrawableRightTint = createTintInfo(context, drawableManager,
84                     a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableRight, 0));
85         }
86         if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableBottom)) {
87             mDrawableBottomTint = createTintInfo(context, drawableManager,
88                     a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableBottom, 0));
89         }
90         a.recycle();
91 
92         // PasswordTransformationMethod wipes out all other TransformationMethod instances
93         // in TextView's constructor, so we should only set a new transformation method
94         // if we don't have a PasswordTransformationMethod currently...
95         final boolean hasPwdTm =
96                 mView.getTransformationMethod() instanceof PasswordTransformationMethod;
97         boolean allCaps = false;
98         boolean allCapsSet = false;
99         ColorStateList textColor = null;
100         ColorStateList textColorHint = null;
101         ColorStateList textColorLink = null;
102 
103         // First check TextAppearance's textAllCaps value
104         if (ap != -1) {
105             a = TintTypedArray.obtainStyledAttributes(context, ap, R.styleable.TextAppearance);
106             if (!hasPwdTm && a.hasValue(R.styleable.TextAppearance_textAllCaps)) {
107                 allCapsSet = true;
108                 allCaps = a.getBoolean(R.styleable.TextAppearance_textAllCaps, false);
109             }
110 
111             updateTypefaceAndStyle(context, a);
112             if (Build.VERSION.SDK_INT < 23) {
113                 // If we're running on < API 23, the text color may contain theme references
114                 // so let's re-set using our own inflater
115                 if (a.hasValue(R.styleable.TextAppearance_android_textColor)) {
116                     textColor = a.getColorStateList(R.styleable.TextAppearance_android_textColor);
117                 }
118                 if (a.hasValue(R.styleable.TextAppearance_android_textColorHint)) {
119                     textColorHint = a.getColorStateList(
120                             R.styleable.TextAppearance_android_textColorHint);
121                 }
122                 if (a.hasValue(R.styleable.TextAppearance_android_textColorLink)) {
123                     textColorLink = a.getColorStateList(
124                             R.styleable.TextAppearance_android_textColorLink);
125                 }
126             }
127             a.recycle();
128         }
129 
130         // Now read the style's values
131         a = TintTypedArray.obtainStyledAttributes(context, attrs, R.styleable.TextAppearance,
132                 defStyleAttr, 0);
133         if (!hasPwdTm && a.hasValue(R.styleable.TextAppearance_textAllCaps)) {
134             allCapsSet = true;
135             allCaps = a.getBoolean(R.styleable.TextAppearance_textAllCaps, false);
136         }
137         if (Build.VERSION.SDK_INT < 23) {
138             // If we're running on < API 23, the text color may contain theme references
139             // so let's re-set using our own inflater
140             if (a.hasValue(R.styleable.TextAppearance_android_textColor)) {
141                 textColor = a.getColorStateList(R.styleable.TextAppearance_android_textColor);
142             }
143             if (a.hasValue(R.styleable.TextAppearance_android_textColorHint)) {
144                 textColorHint = a.getColorStateList(
145                         R.styleable.TextAppearance_android_textColorHint);
146             }
147             if (a.hasValue(R.styleable.TextAppearance_android_textColorLink)) {
148                 textColorLink = a.getColorStateList(
149                         R.styleable.TextAppearance_android_textColorLink);
150             }
151         }
152 
153         updateTypefaceAndStyle(context, a);
154         a.recycle();
155 
156         if (textColor != null) {
157             mView.setTextColor(textColor);
158         }
159         if (textColorHint != null) {
160             mView.setHintTextColor(textColorHint);
161         }
162         if (textColorLink != null) {
163             mView.setLinkTextColor(textColorLink);
164         }
165         if (!hasPwdTm && allCapsSet) {
166             setAllCaps(allCaps);
167         }
168         if (mFontTypeface != null) {
169             mView.setTypeface(mFontTypeface, mStyle);
170         }
171 
172         mAutoSizeTextHelper.loadFromAttributes(attrs, defStyleAttr);
173 
174         if (BuildCompat.isAtLeastO()) {
175             // Delegate auto-size functionality to the framework implementation.
176             if (mAutoSizeTextHelper.getAutoSizeTextType()
177                     != TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE) {
178                 final int[] autoSizeTextSizesInPx =
179                         mAutoSizeTextHelper.getAutoSizeTextAvailableSizes();
180                 if (autoSizeTextSizesInPx.length > 0) {
181                     if (mView.getAutoSizeStepGranularity() != AppCompatTextViewAutoSizeHelper
182                             .UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) {
183                         // Configured with granularity, preserve details.
184                         mView.setAutoSizeTextTypeUniformWithConfiguration(
185                                 mAutoSizeTextHelper.getAutoSizeMinTextSize(),
186                                 mAutoSizeTextHelper.getAutoSizeMaxTextSize(),
187                                 mAutoSizeTextHelper.getAutoSizeStepGranularity(),
188                                 TypedValue.COMPLEX_UNIT_PX);
189                     } else {
190                         mView.setAutoSizeTextTypeUniformWithPresetSizes(
191                                 autoSizeTextSizesInPx, TypedValue.COMPLEX_UNIT_PX);
192                     }
193                 }
194             }
195         }
196     }
197 
updateTypefaceAndStyle(Context context, TintTypedArray a)198     private void updateTypefaceAndStyle(Context context, TintTypedArray a) {
199         mStyle = a.getInt(R.styleable.TextAppearance_android_textStyle, mStyle);
200 
201         if (a.hasValue(R.styleable.TextAppearance_android_fontFamily)
202                 || a.hasValue(R.styleable.TextAppearance_fontFamily)) {
203             int fontFamilyId = a.hasValue(R.styleable.TextAppearance_android_fontFamily)
204                     ? R.styleable.TextAppearance_android_fontFamily
205                     : R.styleable.TextAppearance_fontFamily;
206             if (!context.isRestricted()) {
207                 try {
208                     mFontTypeface = a.getFont(fontFamilyId, mStyle, mView);
209                 } catch (UnsupportedOperationException | Resources.NotFoundException e) {
210                     // Expected if it is not a font resource.
211                 }
212             }
213             if (mFontTypeface == null) {
214                 // Try with String. This is done by TextView JB+, but fails in ICS
215                 String fontFamilyName = a.getString(fontFamilyId);
216                 mFontTypeface = Typeface.create(fontFamilyName, mStyle);
217             }
218         }
219     }
220 
onSetTextAppearance(Context context, int resId)221     void onSetTextAppearance(Context context, int resId) {
222         final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context,
223                 resId, R.styleable.TextAppearance);
224         if (a.hasValue(R.styleable.TextAppearance_textAllCaps)) {
225             // This breaks away slightly from the logic in TextView.setTextAppearance that serves
226             // as an "overlay" on the current state of the TextView. Since android:textAllCaps
227             // may have been set to true in this text appearance, we need to make sure that
228             // app:textAllCaps has the chance to override it
229             setAllCaps(a.getBoolean(R.styleable.TextAppearance_textAllCaps, false));
230         }
231         if (Build.VERSION.SDK_INT < 23
232                 && a.hasValue(R.styleable.TextAppearance_android_textColor)) {
233             // If we're running on < API 23, the text color may contain theme references
234             // so let's re-set using our own inflater
235             final ColorStateList textColor
236                     = a.getColorStateList(R.styleable.TextAppearance_android_textColor);
237             if (textColor != null) {
238                 mView.setTextColor(textColor);
239             }
240         }
241 
242         updateTypefaceAndStyle(context, a);
243         a.recycle();
244         if (mFontTypeface != null) {
245             mView.setTypeface(mFontTypeface, mStyle);
246         }
247     }
248 
setAllCaps(boolean allCaps)249     void setAllCaps(boolean allCaps) {
250         mView.setAllCaps(allCaps);
251     }
252 
applyCompoundDrawablesTints()253     void applyCompoundDrawablesTints() {
254         if (mDrawableLeftTint != null || mDrawableTopTint != null ||
255                 mDrawableRightTint != null || mDrawableBottomTint != null) {
256             final Drawable[] compoundDrawables = mView.getCompoundDrawables();
257             applyCompoundDrawableTint(compoundDrawables[0], mDrawableLeftTint);
258             applyCompoundDrawableTint(compoundDrawables[1], mDrawableTopTint);
259             applyCompoundDrawableTint(compoundDrawables[2], mDrawableRightTint);
260             applyCompoundDrawableTint(compoundDrawables[3], mDrawableBottomTint);
261         }
262     }
263 
applyCompoundDrawableTint(Drawable drawable, TintInfo info)264     final void applyCompoundDrawableTint(Drawable drawable, TintInfo info) {
265         if (drawable != null && info != null) {
266             AppCompatDrawableManager.tintDrawable(drawable, info, mView.getDrawableState());
267         }
268     }
269 
createTintInfo(Context context, AppCompatDrawableManager drawableManager, int drawableId)270     protected static TintInfo createTintInfo(Context context,
271             AppCompatDrawableManager drawableManager, int drawableId) {
272         final ColorStateList tintList = drawableManager.getTintList(context, drawableId);
273         if (tintList != null) {
274             final TintInfo tintInfo = new TintInfo();
275             tintInfo.mHasTintList = true;
276             tintInfo.mTintList = tintList;
277             return tintInfo;
278         }
279         return null;
280     }
281 
282     /** @hide */
283     @RestrictTo(LIBRARY_GROUP)
onLayout(boolean changed, int left, int top, int right, int bottom)284     void onLayout(boolean changed, int left, int top, int right, int bottom) {
285         // Auto-size is supported by the framework starting from Android O.
286         if (!BuildCompat.isAtLeastO()) {
287             if (isAutoSizeEnabled()) {
288                 if (getNeedsAutoSizeText()) {
289                     // Call auto-size after the width and height have been calculated.
290                     autoSizeText();
291                 }
292                 // Always try to auto-size if enabled. Functions that do not want to trigger
293                 // auto-sizing after the next layout round should set this to false.
294                 setNeedsAutoSizeText(true);
295             }
296         }
297     }
298 
299     /** @hide */
300     @RestrictTo(LIBRARY_GROUP)
setTextSize(int unit, float size)301     void setTextSize(int unit, float size) {
302         if (!BuildCompat.isAtLeastO()) {
303             if (!isAutoSizeEnabled()) {
304                 setTextSizeInternal(unit, size);
305             }
306         }
307     }
308 
isAutoSizeEnabled()309     private boolean isAutoSizeEnabled() {
310         return mAutoSizeTextHelper.isAutoSizeEnabled();
311     }
312 
getNeedsAutoSizeText()313     private boolean getNeedsAutoSizeText() {
314         return mAutoSizeTextHelper.getNeedsAutoSizeText();
315     }
316 
setNeedsAutoSizeText(boolean needsAutoSizeText)317     private void setNeedsAutoSizeText(boolean needsAutoSizeText) {
318         mAutoSizeTextHelper.setNeedsAutoSizeText(needsAutoSizeText);
319     }
320 
autoSizeText()321     private void autoSizeText() {
322         mAutoSizeTextHelper.autoSizeText();
323     }
324 
setTextSizeInternal(int unit, float size)325     private void setTextSizeInternal(int unit, float size) {
326         mAutoSizeTextHelper.setTextSizeInternal(unit, size);
327     }
328 
setAutoSizeTextTypeWithDefaults(@extViewCompat.AutoSizeTextType int autoSizeTextType)329     void setAutoSizeTextTypeWithDefaults(@TextViewCompat.AutoSizeTextType int autoSizeTextType) {
330         mAutoSizeTextHelper.setAutoSizeTextTypeWithDefaults(autoSizeTextType);
331     }
332 
setAutoSizeTextTypeUniformWithConfiguration( int autoSizeMinTextSize, int autoSizeMaxTextSize, int autoSizeStepGranularity, int unit)333     void setAutoSizeTextTypeUniformWithConfiguration(
334             int autoSizeMinTextSize,
335             int autoSizeMaxTextSize,
336             int autoSizeStepGranularity,
337             int unit) throws IllegalArgumentException {
338         mAutoSizeTextHelper.setAutoSizeTextTypeUniformWithConfiguration(
339                 autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit);
340     }
341 
setAutoSizeTextTypeUniformWithPresetSizes(@onNull int[] presetSizes, int unit)342     void setAutoSizeTextTypeUniformWithPresetSizes(@NonNull int[] presetSizes, int unit)
343             throws IllegalArgumentException {
344         mAutoSizeTextHelper.setAutoSizeTextTypeUniformWithPresetSizes(presetSizes, unit);
345     }
346 
347     @TextViewCompat.AutoSizeTextType
getAutoSizeTextType()348     int getAutoSizeTextType() {
349         return mAutoSizeTextHelper.getAutoSizeTextType();
350     }
351 
getAutoSizeStepGranularity()352     int getAutoSizeStepGranularity() {
353         return mAutoSizeTextHelper.getAutoSizeStepGranularity();
354     }
355 
getAutoSizeMinTextSize()356     int getAutoSizeMinTextSize() {
357         return mAutoSizeTextHelper.getAutoSizeMinTextSize();
358     }
359 
getAutoSizeMaxTextSize()360     int getAutoSizeMaxTextSize() {
361         return mAutoSizeTextHelper.getAutoSizeMaxTextSize();
362     }
363 
getAutoSizeTextAvailableSizes()364     int[] getAutoSizeTextAvailableSizes() {
365         return mAutoSizeTextHelper.getAutoSizeTextAvailableSizes();
366     }
367 }
368