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;
20
21import android.content.Context;
22import android.content.res.ColorStateList;
23import android.content.res.Resources;
24import android.graphics.Typeface;
25import android.graphics.drawable.Drawable;
26import android.os.Build;
27import android.support.annotation.NonNull;
28import android.support.annotation.RequiresApi;
29import android.support.annotation.RestrictTo;
30import android.support.v4.os.BuildCompat;
31import android.support.v4.widget.TextViewCompat;
32import android.support.v7.appcompat.R;
33import android.text.method.PasswordTransformationMethod;
34import android.util.AttributeSet;
35import android.util.TypedValue;
36import android.widget.TextView;
37
38@RequiresApi(9)
39class AppCompatTextHelper {
40
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
60    AppCompatTextHelper(TextView view) {
61        mView = view;
62        mAutoSizeTextHelper = new AppCompatTextViewAutoSizeHelper(mView);
63    }
64
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
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
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
249    void setAllCaps(boolean allCaps) {
250        mView.setAllCaps(allCaps);
251    }
252
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
264    final void applyCompoundDrawableTint(Drawable drawable, TintInfo info) {
265        if (drawable != null && info != null) {
266            AppCompatDrawableManager.tintDrawable(drawable, info, mView.getDrawableState());
267        }
268    }
269
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)
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)
301    void setTextSize(int unit, float size) {
302        if (!BuildCompat.isAtLeastO()) {
303            if (!isAutoSizeEnabled()) {
304                setTextSizeInternal(unit, size);
305            }
306        }
307    }
308
309    private boolean isAutoSizeEnabled() {
310        return mAutoSizeTextHelper.isAutoSizeEnabled();
311    }
312
313    private boolean getNeedsAutoSizeText() {
314        return mAutoSizeTextHelper.getNeedsAutoSizeText();
315    }
316
317    private void setNeedsAutoSizeText(boolean needsAutoSizeText) {
318        mAutoSizeTextHelper.setNeedsAutoSizeText(needsAutoSizeText);
319    }
320
321    private void autoSizeText() {
322        mAutoSizeTextHelper.autoSizeText();
323    }
324
325    private void setTextSizeInternal(int unit, float size) {
326        mAutoSizeTextHelper.setTextSizeInternal(unit, size);
327    }
328
329    void setAutoSizeTextTypeWithDefaults(@TextViewCompat.AutoSizeTextType int autoSizeTextType) {
330        mAutoSizeTextHelper.setAutoSizeTextTypeWithDefaults(autoSizeTextType);
331    }
332
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
342    void setAutoSizeTextTypeUniformWithPresetSizes(@NonNull int[] presetSizes, int unit)
343            throws IllegalArgumentException {
344        mAutoSizeTextHelper.setAutoSizeTextTypeUniformWithPresetSizes(presetSizes, unit);
345    }
346
347    @TextViewCompat.AutoSizeTextType
348    int getAutoSizeTextType() {
349        return mAutoSizeTextHelper.getAutoSizeTextType();
350    }
351
352    int getAutoSizeStepGranularity() {
353        return mAutoSizeTextHelper.getAutoSizeStepGranularity();
354    }
355
356    int getAutoSizeMinTextSize() {
357        return mAutoSizeTextHelper.getAutoSizeMinTextSize();
358    }
359
360    int getAutoSizeMaxTextSize() {
361        return mAutoSizeTextHelper.getAutoSizeMaxTextSize();
362    }
363
364    int[] getAutoSizeTextAvailableSizes() {
365        return mAutoSizeTextHelper.getAutoSizeTextAvailableSizes();
366    }
367}
368