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 static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
20 
21 import android.content.res.ColorStateList;
22 import android.graphics.PorterDuff;
23 import android.graphics.drawable.Drawable;
24 import android.os.Build;
25 import android.util.AttributeSet;
26 import android.widget.ImageView;
27 
28 import androidx.annotation.RestrictTo;
29 import androidx.appcompat.R;
30 import androidx.appcompat.content.res.AppCompatResources;
31 import androidx.core.view.ViewCompat;
32 import androidx.core.widget.ImageViewCompat;
33 
34 import org.jspecify.annotations.NonNull;
35 
36 /**
37  */
38 @RestrictTo(LIBRARY_GROUP_PREFIX)
39 public class AppCompatImageHelper {
40     private final @NonNull ImageView mView;
41 
42     private TintInfo mInternalImageTint;
43     private TintInfo mImageTint;
44     private TintInfo mTmpInfo;
45     private int mLevel = 0;
46 
AppCompatImageHelper(@onNull ImageView view)47     public AppCompatImageHelper(@NonNull ImageView view) {
48         mView = view;
49     }
50 
loadFromAttributes(AttributeSet attrs, int defStyleAttr)51     public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
52         TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs,
53                 R.styleable.AppCompatImageView, defStyleAttr, 0);
54         ViewCompat.saveAttributeDataForStyleable(mView, mView.getContext(),
55                 R.styleable.AppCompatImageView, attrs, a.getWrappedTypeArray(), defStyleAttr, 0);
56         try {
57             Drawable drawable = mView.getDrawable();
58             if (drawable == null) {
59                 // If the view doesn't already have a drawable (from android:src), try loading
60                 // it from srcCompat
61                 final int id = a.getResourceId(R.styleable.AppCompatImageView_srcCompat, -1);
62                 if (id != -1) {
63                     drawable = AppCompatResources.getDrawable(mView.getContext(), id);
64                     if (drawable != null) {
65                         mView.setImageDrawable(drawable);
66                     }
67                 }
68             }
69 
70             if (drawable != null) {
71                 DrawableUtils.fixDrawable(drawable);
72             }
73 
74             if (a.hasValue(R.styleable.AppCompatImageView_tint)) {
75                 ImageViewCompat.setImageTintList(mView,
76                         a.getColorStateList(R.styleable.AppCompatImageView_tint));
77             }
78             if (a.hasValue(R.styleable.AppCompatImageView_tintMode)) {
79                 ImageViewCompat.setImageTintMode(mView,
80                         DrawableUtils.parseTintMode(
81                                 a.getInt(R.styleable.AppCompatImageView_tintMode, -1), null));
82             }
83         } finally {
84             a.recycle();
85         }
86     }
87 
setImageResource(int resId)88     public void setImageResource(int resId) {
89         if (resId != 0) {
90             final Drawable d = AppCompatResources.getDrawable(mView.getContext(), resId);
91             if (d != null) {
92                 DrawableUtils.fixDrawable(d);
93             }
94             mView.setImageDrawable(d);
95         } else {
96             mView.setImageDrawable(null);
97         }
98 
99         applySupportImageTint();
100     }
101 
hasOverlappingRendering()102     boolean hasOverlappingRendering() {
103         final Drawable background = mView.getBackground();
104         if (Build.VERSION.SDK_INT >= 21
105                 && background instanceof android.graphics.drawable.RippleDrawable) {
106             // RippleDrawable has an issue on L+ when used with an alpha animation.
107             // This workaround should be disabled when the platform bug is fixed. See b/27715789
108             return false;
109         }
110         return true;
111     }
112 
setSupportImageTintList(ColorStateList tint)113     void setSupportImageTintList(ColorStateList tint) {
114         if (mImageTint == null) {
115             mImageTint = new TintInfo();
116         }
117         mImageTint.mTintList = tint;
118         mImageTint.mHasTintList = true;
119         applySupportImageTint();
120     }
121 
getSupportImageTintList()122     ColorStateList getSupportImageTintList() {
123         return mImageTint != null ? mImageTint.mTintList : null;
124     }
125 
setSupportImageTintMode(PorterDuff.Mode tintMode)126     void setSupportImageTintMode(PorterDuff.Mode tintMode) {
127         if (mImageTint == null) {
128             mImageTint = new TintInfo();
129         }
130         mImageTint.mTintMode = tintMode;
131         mImageTint.mHasTintMode = true;
132 
133         applySupportImageTint();
134     }
135 
getSupportImageTintMode()136     PorterDuff.Mode getSupportImageTintMode() {
137         return mImageTint != null ? mImageTint.mTintMode : null;
138     }
139 
applySupportImageTint()140     void applySupportImageTint() {
141         final Drawable imageViewDrawable = mView.getDrawable();
142         if (imageViewDrawable != null) {
143             DrawableUtils.fixDrawable(imageViewDrawable);
144         }
145 
146         if (imageViewDrawable != null) {
147             if (shouldApplyFrameworkTintUsingColorFilter()
148                     && applyFrameworkTintUsingColorFilter(imageViewDrawable)) {
149                 // This needs to be called before the internal tints below so it takes
150                 // effect on any widgets using the compat tint on API 21
151                 return;
152             }
153 
154             if (mImageTint != null) {
155                 AppCompatDrawableManager.tintDrawable(imageViewDrawable, mImageTint,
156                         mView.getDrawableState());
157             } else if (mInternalImageTint != null) {
158                 AppCompatDrawableManager.tintDrawable(imageViewDrawable, mInternalImageTint,
159                         mView.getDrawableState());
160             }
161         }
162     }
163 
setInternalImageTint(ColorStateList tint)164     void setInternalImageTint(ColorStateList tint) {
165         if (tint != null) {
166             if (mInternalImageTint == null) {
167                 mInternalImageTint = new TintInfo();
168             }
169             mInternalImageTint.mTintList = tint;
170             mInternalImageTint.mHasTintList = true;
171         } else {
172             mInternalImageTint = null;
173         }
174         applySupportImageTint();
175     }
176 
shouldApplyFrameworkTintUsingColorFilter()177     private boolean shouldApplyFrameworkTintUsingColorFilter() {
178         final int sdk = Build.VERSION.SDK_INT;
179         if (sdk > 21) {
180             // On API 22+, if we're using an internal compat image source tint, we're also
181             // responsible for applying any custom tint set via the framework impl
182             return mInternalImageTint != null;
183         } else if (sdk == 21) {
184             // GradientDrawable doesn't implement setTintList on API 21, and since there is
185             // no nice way to unwrap DrawableContainers we have to blanket apply this
186             // on API 21
187             return true;
188         } else {
189             // API 19 and below doesn't have framework tint
190             return false;
191         }
192     }
193 
194     /**
195      * Applies the framework image source tint to a view, but using the compat method (ColorFilter)
196      *
197      * @return true if a tint was applied
198      */
applyFrameworkTintUsingColorFilter(@onNull Drawable imageSource)199     private boolean applyFrameworkTintUsingColorFilter(@NonNull Drawable imageSource) {
200         if (mTmpInfo == null) {
201             mTmpInfo = new TintInfo();
202         }
203         final TintInfo info = mTmpInfo;
204         info.clear();
205 
206         final ColorStateList tintList = ImageViewCompat.getImageTintList(mView);
207         if (tintList != null) {
208             info.mHasTintList = true;
209             info.mTintList = tintList;
210         }
211         final PorterDuff.Mode mode = ImageViewCompat.getImageTintMode(mView);
212         if (mode != null) {
213             info.mHasTintMode = true;
214             info.mTintMode = mode;
215         }
216 
217         if (info.mHasTintList || info.mHasTintMode) {
218             AppCompatDrawableManager.tintDrawable(imageSource, info, mView.getDrawableState());
219             return true;
220         }
221 
222         return false;
223     }
224 
225     /**
226      * Extracts the level from the drawable parameter and save it for the future use in
227      * {@link #applyImageLevel()}
228      */
obtainLevelFromDrawable(@onNull Drawable drawable)229     void obtainLevelFromDrawable(@NonNull Drawable drawable) {
230         mLevel = drawable.getLevel();
231     }
232 
233     /**
234      * Applies the level previously set through {@link #obtainLevelFromDrawable}
235      */
applyImageLevel()236     void applyImageLevel() {
237         if (mView.getDrawable() != null) {
238             mView.getDrawable().setLevel(mLevel);
239         }
240     }
241 }
242