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 android.content.res.ColorStateList;
20import android.graphics.PorterDuff;
21import android.graphics.drawable.Drawable;
22import android.os.Build;
23import android.support.annotation.NonNull;
24import android.support.v4.view.ViewCompat;
25import android.support.v7.appcompat.R;
26import android.util.AttributeSet;
27import android.view.View;
28
29class AppCompatBackgroundHelper {
30
31    private final View mView;
32    private final AppCompatDrawableManager mDrawableManager;
33
34    private int mBackgroundResId = -1;
35
36    private BackgroundTintInfo mInternalBackgroundTint;
37    private BackgroundTintInfo mBackgroundTint;
38    private BackgroundTintInfo mTmpInfo;
39
40    AppCompatBackgroundHelper(View view) {
41        mView = view;
42        mDrawableManager = AppCompatDrawableManager.get();
43    }
44
45    void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
46        TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs,
47                R.styleable.ViewBackgroundHelper, defStyleAttr, 0);
48        try {
49            if (a.hasValue(R.styleable.ViewBackgroundHelper_android_background)) {
50                mBackgroundResId = a.getResourceId(
51                        R.styleable.ViewBackgroundHelper_android_background, -1);
52                ColorStateList tint = mDrawableManager
53                        .getTintList(mView.getContext(), mBackgroundResId);
54                if (tint != null) {
55                    setInternalBackgroundTint(tint);
56                }
57            }
58            if (a.hasValue(R.styleable.ViewBackgroundHelper_backgroundTint)) {
59                ViewCompat.setBackgroundTintList(mView,
60                        a.getColorStateList(R.styleable.ViewBackgroundHelper_backgroundTint));
61            }
62            if (a.hasValue(R.styleable.ViewBackgroundHelper_backgroundTintMode)) {
63                ViewCompat.setBackgroundTintMode(mView,
64                        DrawableUtils.parseTintMode(
65                                a.getInt(R.styleable.ViewBackgroundHelper_backgroundTintMode, -1),
66                                null));
67            }
68        } finally {
69            a.recycle();
70        }
71    }
72
73    void onSetBackgroundResource(int resId) {
74        mBackgroundResId = resId;
75        // Update the default background tint
76        setInternalBackgroundTint(mDrawableManager != null
77                ? mDrawableManager.getTintList(mView.getContext(), resId)
78                : null);
79
80        if (updateBackgroundTint()) {
81            applySupportBackgroundTint();
82        }
83    }
84
85    void onSetBackgroundDrawable(Drawable background) {
86        mBackgroundResId = -1;
87        // We don't know that this drawable is, so we need to clear the default background tint
88        setInternalBackgroundTint(null);
89
90        if (updateBackgroundTint()) {
91            applySupportBackgroundTint();
92        }
93    }
94
95    void setSupportBackgroundTintList(ColorStateList tint) {
96        if (mBackgroundTint == null) {
97            mBackgroundTint = new BackgroundTintInfo();
98        }
99
100        // Store the original tint and null out the applicable tint. updateBackgroundTint() will
101        // set mTintList to the tint to actually use
102        mBackgroundTint.mOriginalTintList = tint;
103        mBackgroundTint.mTintList = null;
104        mBackgroundTint.mHasTintList = true;
105
106        if (updateBackgroundTint()) {
107            applySupportBackgroundTint();
108        }
109    }
110
111    /**
112     * Updates the background tint state
113     * @return true if the state was changed and requires an apply
114     */
115    private boolean updateBackgroundTint() {
116        if (mBackgroundTint != null && mBackgroundTint.mHasTintList) {
117            if (mBackgroundResId >= 0) {
118                // If we have a background resource id, lets see if we need to modify the tint
119                // list to add any touch highlights in (for example, Button needs this)
120                final ColorStateList updated = mDrawableManager.getTintList(
121                        mView.getContext(), mBackgroundResId, mBackgroundTint.mOriginalTintList);
122                if (updated != null) {
123                    mBackgroundTint.mTintList = updated;
124                    return true;
125                }
126            }
127            // If we reach here then we should just be using the original tint list. Check if we
128            // need to set and apply
129            if (mBackgroundTint.mTintList != mBackgroundTint.mOriginalTintList) {
130                mBackgroundTint.mTintList = mBackgroundTint.mOriginalTintList;
131                return true;
132            }
133        }
134        return false;
135    }
136
137    ColorStateList getSupportBackgroundTintList() {
138        return mBackgroundTint != null ? mBackgroundTint.mTintList : null;
139    }
140
141    void setSupportBackgroundTintMode(PorterDuff.Mode tintMode) {
142        if (mBackgroundTint == null) {
143            mBackgroundTint = new BackgroundTintInfo();
144        }
145        mBackgroundTint.mTintMode = tintMode;
146        mBackgroundTint.mHasTintMode = true;
147
148        applySupportBackgroundTint();
149    }
150
151    PorterDuff.Mode getSupportBackgroundTintMode() {
152        return mBackgroundTint != null ? mBackgroundTint.mTintMode : null;
153    }
154
155    void applySupportBackgroundTint() {
156        final Drawable background = mView.getBackground();
157        if (background != null) {
158            if (Build.VERSION.SDK_INT == 21 && applyFrameworkTintUsingColorFilter(background)) {
159                // GradientDrawable doesn't implement setTintList on API 21, and since there is
160                // no nice way to unwrap DrawableContainers we have to blanket apply this
161                // on API 21. This needs to be called before the internal tints below so it takes
162                // effect on any widgets using the compat tint on API 21 (EditText)
163                return;
164            }
165
166            if (mBackgroundTint != null) {
167                AppCompatDrawableManager.tintDrawable(background, mBackgroundTint,
168                        mView.getDrawableState());
169            } else if (mInternalBackgroundTint != null) {
170                AppCompatDrawableManager.tintDrawable(background, mInternalBackgroundTint,
171                        mView.getDrawableState());
172            }
173        }
174    }
175
176    void setInternalBackgroundTint(ColorStateList tint) {
177        if (tint != null) {
178            if (mInternalBackgroundTint == null) {
179                mInternalBackgroundTint = new BackgroundTintInfo();
180            }
181            mInternalBackgroundTint.mTintList = tint;
182            mInternalBackgroundTint.mHasTintList = true;
183        } else {
184            mInternalBackgroundTint = null;
185        }
186        applySupportBackgroundTint();
187    }
188
189    /**
190     * Applies the framework background tint to a view, but using the compat method (ColorFilter)
191     *
192     * @return true if a tint was applied
193     */
194    private boolean applyFrameworkTintUsingColorFilter(@NonNull Drawable background) {
195        if (mTmpInfo == null) {
196            mTmpInfo = new BackgroundTintInfo();
197        }
198        final TintInfo info = mTmpInfo;
199        info.clear();
200
201        final ColorStateList tintList = ViewCompat.getBackgroundTintList(mView);
202        if (tintList != null) {
203            info.mHasTintList = true;
204            info.mTintList = tintList;
205        }
206        final PorterDuff.Mode mode = ViewCompat.getBackgroundTintMode(mView);
207        if (mode != null) {
208            info.mHasTintMode = true;
209            info.mTintMode = mode;
210        }
211
212        if (info.mHasTintList || info.mHasTintMode) {
213            AppCompatDrawableManager.tintDrawable(background, info, mView.getDrawableState());
214            return true;
215        }
216
217        return false;
218    }
219
220    private static class BackgroundTintInfo extends TintInfo {
221        // The original tint list given to the call. We need this distinction because create a
222        // modified for actual tinting purposes
223        public ColorStateList mOriginalTintList;
224
225        BackgroundTintInfo() {
226        }
227
228        @Override
229        void clear() {
230            super.clear();
231            mOriginalTintList = null;
232        }
233    }
234}
235