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 android.content.res.ColorStateList;
20 import android.graphics.PorterDuff;
21 import android.graphics.drawable.Drawable;
22 import android.os.Build;
23 import android.util.AttributeSet;
24 import android.view.View;
25 
26 import androidx.appcompat.R;
27 import androidx.core.view.ViewCompat;
28 
29 import org.jspecify.annotations.NonNull;
30 import org.jspecify.annotations.Nullable;
31 
32 class AppCompatBackgroundHelper {
33 
34     private final @NonNull View mView;
35     private final AppCompatDrawableManager mDrawableManager;
36 
37     private int mBackgroundResId = -1;
38 
39     private TintInfo mInternalBackgroundTint;
40     private TintInfo mBackgroundTint;
41     private TintInfo mTmpInfo;
42 
AppCompatBackgroundHelper(@onNull View view)43     AppCompatBackgroundHelper(@NonNull View view) {
44         mView = view;
45         mDrawableManager = AppCompatDrawableManager.get();
46     }
47 
loadFromAttributes(@ullable AttributeSet attrs, int defStyleAttr)48     void loadFromAttributes(@Nullable AttributeSet attrs, int defStyleAttr) {
49         TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs,
50                 R.styleable.ViewBackgroundHelper, defStyleAttr, 0);
51         ViewCompat.saveAttributeDataForStyleable(mView, mView.getContext(),
52                 R.styleable.ViewBackgroundHelper, attrs, a.getWrappedTypeArray(),
53                 defStyleAttr, 0);
54         try {
55             if (a.hasValue(R.styleable.ViewBackgroundHelper_android_background)) {
56                 mBackgroundResId = a.getResourceId(
57                         R.styleable.ViewBackgroundHelper_android_background, -1);
58                 ColorStateList tint = mDrawableManager
59                         .getTintList(mView.getContext(), mBackgroundResId);
60                 if (tint != null) {
61                     setInternalBackgroundTint(tint);
62                 }
63             }
64             if (a.hasValue(R.styleable.ViewBackgroundHelper_backgroundTint)) {
65                 ViewCompat.setBackgroundTintList(mView,
66                         a.getColorStateList(R.styleable.ViewBackgroundHelper_backgroundTint));
67             }
68             if (a.hasValue(R.styleable.ViewBackgroundHelper_backgroundTintMode)) {
69                 ViewCompat.setBackgroundTintMode(mView,
70                         DrawableUtils.parseTintMode(
71                                 a.getInt(R.styleable.ViewBackgroundHelper_backgroundTintMode, -1),
72                                 null));
73             }
74         } finally {
75             a.recycle();
76         }
77     }
78 
onSetBackgroundResource(int resId)79     void onSetBackgroundResource(int resId) {
80         mBackgroundResId = resId;
81         // Update the default background tint
82         setInternalBackgroundTint(mDrawableManager != null
83                 ? mDrawableManager.getTintList(mView.getContext(), resId)
84                 : null);
85         applySupportBackgroundTint();
86     }
87 
onSetBackgroundDrawable(Drawable background)88     void onSetBackgroundDrawable(Drawable background) {
89         mBackgroundResId = -1;
90         // We don't know that this drawable is, so we need to clear the default background tint
91         setInternalBackgroundTint(null);
92         applySupportBackgroundTint();
93     }
94 
setSupportBackgroundTintList(ColorStateList tint)95     void setSupportBackgroundTintList(ColorStateList tint) {
96         if (mBackgroundTint == null) {
97             mBackgroundTint = new TintInfo();
98         }
99         mBackgroundTint.mTintList = tint;
100         mBackgroundTint.mHasTintList = true;
101         applySupportBackgroundTint();
102     }
103 
getSupportBackgroundTintList()104     ColorStateList getSupportBackgroundTintList() {
105         return mBackgroundTint != null ? mBackgroundTint.mTintList : null;
106     }
107 
setSupportBackgroundTintMode(PorterDuff.Mode tintMode)108     void setSupportBackgroundTintMode(PorterDuff.Mode tintMode) {
109         if (mBackgroundTint == null) {
110             mBackgroundTint = new TintInfo();
111         }
112         mBackgroundTint.mTintMode = tintMode;
113         mBackgroundTint.mHasTintMode = true;
114 
115         applySupportBackgroundTint();
116     }
117 
getSupportBackgroundTintMode()118     PorterDuff.Mode getSupportBackgroundTintMode() {
119         return mBackgroundTint != null ? mBackgroundTint.mTintMode : null;
120     }
121 
applySupportBackgroundTint()122     void applySupportBackgroundTint() {
123         final Drawable background = mView.getBackground();
124         if (background != null) {
125             if (shouldApplyFrameworkTintUsingColorFilter()
126                     && applyFrameworkTintUsingColorFilter(background)) {
127                 // This needs to be called before the internal tints below so it takes
128                 // effect on any widgets using the compat tint on API 21 (EditText)
129                 return;
130             }
131 
132             if (mBackgroundTint != null) {
133                 AppCompatDrawableManager.tintDrawable(background, mBackgroundTint,
134                         mView.getDrawableState());
135             } else if (mInternalBackgroundTint != null) {
136                 AppCompatDrawableManager.tintDrawable(background, mInternalBackgroundTint,
137                         mView.getDrawableState());
138             }
139         }
140     }
141 
setInternalBackgroundTint(ColorStateList tint)142     void setInternalBackgroundTint(ColorStateList tint) {
143         if (tint != null) {
144             if (mInternalBackgroundTint == null) {
145                 mInternalBackgroundTint = new TintInfo();
146             }
147             mInternalBackgroundTint.mTintList = tint;
148             mInternalBackgroundTint.mHasTintList = true;
149         } else {
150             mInternalBackgroundTint = null;
151         }
152         applySupportBackgroundTint();
153     }
154 
shouldApplyFrameworkTintUsingColorFilter()155     private boolean shouldApplyFrameworkTintUsingColorFilter() {
156         final int sdk = Build.VERSION.SDK_INT;
157         if (sdk > 21) {
158             // On API 22+, if we're using an internal compat background tint, we're also
159             // responsible for applying any custom tint set via the framework impl
160             return mInternalBackgroundTint != null;
161         } else if (sdk == 21) {
162             // GradientDrawable doesn't implement setTintList on API 21, and since there is
163             // no nice way to unwrap DrawableContainers we have to blanket apply this
164             // on API 21
165             return true;
166         } else {
167             // API 19 and below doesn't have framework tint
168             return false;
169         }
170     }
171 
172     /**
173      * Applies the framework background tint to a view, but using the compat method (ColorFilter)
174      *
175      * @return true if a tint was applied
176      */
applyFrameworkTintUsingColorFilter(@onNull Drawable background)177     private boolean applyFrameworkTintUsingColorFilter(@NonNull Drawable background) {
178         if (mTmpInfo == null) {
179             mTmpInfo = new TintInfo();
180         }
181         final TintInfo info = mTmpInfo;
182         info.clear();
183 
184         final ColorStateList tintList = ViewCompat.getBackgroundTintList(mView);
185         if (tintList != null) {
186             info.mHasTintList = true;
187             info.mTintList = tintList;
188         }
189         final PorterDuff.Mode mode = ViewCompat.getBackgroundTintMode(mView);
190         if (mode != null) {
191             info.mHasTintMode = true;
192             info.mTintMode = mode;
193         }
194 
195         if (info.mHasTintList || info.mHasTintMode) {
196             AppCompatDrawableManager.tintDrawable(background, info, mView.getDrawableState());
197             return true;
198         }
199 
200         return false;
201     }
202 }
203