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