• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 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.core.graphics.drawable;
18 
19 import android.content.res.ColorStateList;
20 import android.content.res.Resources;
21 import android.graphics.ColorFilter;
22 import android.graphics.PorterDuff;
23 import android.graphics.drawable.Drawable;
24 import android.graphics.drawable.DrawableContainer;
25 import android.graphics.drawable.InsetDrawable;
26 import android.os.Build;
27 import android.util.AttributeSet;
28 import android.util.Log;
29 
30 import androidx.annotation.ColorInt;
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 import androidx.core.view.ViewCompat;
34 
35 import org.xmlpull.v1.XmlPullParser;
36 import org.xmlpull.v1.XmlPullParserException;
37 
38 import java.io.IOException;
39 import java.lang.reflect.Method;
40 
41 /**
42  * Helper for accessing features in {@link android.graphics.drawable.Drawable}.
43  */
44 public final class DrawableCompat {
45     private static final String TAG = "DrawableCompat";
46 
47     private static Method sSetLayoutDirectionMethod;
48     private static boolean sSetLayoutDirectionMethodFetched;
49 
50     private static Method sGetLayoutDirectionMethod;
51     private static boolean sGetLayoutDirectionMethodFetched;
52 
53     /**
54      * Call {@link Drawable#jumpToCurrentState() Drawable.jumpToCurrentState()}.
55      *
56      * @param drawable The Drawable against which to invoke the method.
57      *
58      * @deprecated Use {@link Drawable#jumpToCurrentState()} directly.
59      */
60     @Deprecated
jumpToCurrentState(@onNull Drawable drawable)61     public static void jumpToCurrentState(@NonNull Drawable drawable) {
62         drawable.jumpToCurrentState();
63     }
64 
65     /**
66      * Set whether this Drawable is automatically mirrored when its layout
67      * direction is RTL (right-to left). See
68      * {@link android.util.LayoutDirection}.
69      * <p>
70      * If running on a pre-{@link android.os.Build.VERSION_CODES#KITKAT} device
71      * this method does nothing.
72      *
73      * @param drawable The Drawable against which to invoke the method.
74      * @param mirrored Set to true if the Drawable should be mirrored, false if
75      *            not.
76      */
setAutoMirrored(@onNull Drawable drawable, boolean mirrored)77     public static void setAutoMirrored(@NonNull Drawable drawable, boolean mirrored) {
78         if (Build.VERSION.SDK_INT >= 19) {
79             drawable.setAutoMirrored(mirrored);
80         }
81     }
82 
83     /**
84      * Tells if this Drawable will be automatically mirrored when its layout
85      * direction is RTL right-to-left. See {@link android.util.LayoutDirection}.
86      * <p>
87      * If running on a pre-{@link android.os.Build.VERSION_CODES#KITKAT} device
88      * this method returns false.
89      *
90      * @param drawable The Drawable against which to invoke the method.
91      * @return boolean Returns true if this Drawable will be automatically
92      *         mirrored.
93      */
isAutoMirrored(@onNull Drawable drawable)94     public static boolean isAutoMirrored(@NonNull Drawable drawable) {
95         if (Build.VERSION.SDK_INT >= 19) {
96             return drawable.isAutoMirrored();
97         } else {
98             return false;
99         }
100     }
101 
102     /**
103      * Specifies the hotspot's location within the drawable.
104      *
105      * @param drawable The Drawable against which to invoke the method.
106      * @param x The X coordinate of the center of the hotspot
107      * @param y The Y coordinate of the center of the hotspot
108      */
setHotspot(@onNull Drawable drawable, float x, float y)109     public static void setHotspot(@NonNull Drawable drawable, float x, float y) {
110         if (Build.VERSION.SDK_INT >= 21) {
111             drawable.setHotspot(x, y);
112         }
113     }
114 
115     /**
116      * Sets the bounds to which the hotspot is constrained, if they should be
117      * different from the drawable bounds.
118      *
119      * @param drawable The Drawable against which to invoke the method.
120      */
setHotspotBounds(@onNull Drawable drawable, int left, int top, int right, int bottom)121     public static void setHotspotBounds(@NonNull Drawable drawable, int left, int top,
122             int right, int bottom) {
123         if (Build.VERSION.SDK_INT >= 21) {
124             drawable.setHotspotBounds(left, top, right, bottom);
125         }
126     }
127 
128     /**
129      * Specifies a tint for {@code drawable}.
130      *
131      * @param drawable The Drawable against which to invoke the method.
132      * @param tint     Color to use for tinting this drawable
133      */
setTint(@onNull Drawable drawable, @ColorInt int tint)134     public static void setTint(@NonNull Drawable drawable, @ColorInt int tint) {
135         if (Build.VERSION.SDK_INT >= 21) {
136             drawable.setTint(tint);
137         } else if (drawable instanceof TintAwareDrawable) {
138             ((TintAwareDrawable) drawable).setTint(tint);
139         }
140     }
141 
142     /**
143      * Specifies a tint for {@code drawable} as a color state list.
144      *
145      * @param drawable The Drawable against which to invoke the method.
146      * @param tint     Color state list to use for tinting this drawable, or null to clear the tint
147      */
setTintList(@onNull Drawable drawable, @Nullable ColorStateList tint)148     public static void setTintList(@NonNull Drawable drawable, @Nullable ColorStateList tint) {
149         if (Build.VERSION.SDK_INT >= 21) {
150             drawable.setTintList(tint);
151         } else if (drawable instanceof TintAwareDrawable) {
152             ((TintAwareDrawable) drawable).setTintList(tint);
153         }
154     }
155 
156     /**
157      * Specifies a tint blending mode for {@code drawable}.
158      *
159      * @param drawable The Drawable against which to invoke the method.
160      * @param tintMode A Porter-Duff blending mode
161      */
setTintMode(@onNull Drawable drawable, @NonNull PorterDuff.Mode tintMode)162     public static void setTintMode(@NonNull Drawable drawable, @NonNull PorterDuff.Mode tintMode) {
163         if (Build.VERSION.SDK_INT >= 21) {
164             drawable.setTintMode(tintMode);
165         } else if (drawable instanceof TintAwareDrawable) {
166             ((TintAwareDrawable) drawable).setTintMode(tintMode);
167         }
168     }
169 
170     /**
171      * Get the alpha value of the {@code drawable}.
172      * 0 means fully transparent, 255 means fully opaque.
173      *
174      * @param drawable The Drawable against which to invoke the method.
175      */
getAlpha(@onNull Drawable drawable)176     public static int getAlpha(@NonNull Drawable drawable) {
177         if (Build.VERSION.SDK_INT >= 19) {
178             return drawable.getAlpha();
179         } else {
180             return 0;
181         }
182     }
183 
184     /**
185      * Applies the specified theme to this Drawable and its children.
186      */
applyTheme(@onNull Drawable drawable, @NonNull Resources.Theme theme)187     public static void applyTheme(@NonNull Drawable drawable, @NonNull Resources.Theme theme) {
188         if (Build.VERSION.SDK_INT >= 21) {
189             drawable.applyTheme(theme);
190         }
191     }
192 
193     /**
194      * Whether a theme can be applied to this Drawable and its children.
195      */
canApplyTheme(@onNull Drawable drawable)196     public static boolean canApplyTheme(@NonNull Drawable drawable) {
197         if (Build.VERSION.SDK_INT >= 21) {
198             return drawable.canApplyTheme();
199         } else {
200             return false;
201         }
202     }
203 
204     /**
205      * Returns the current color filter, or {@code null} if none set.
206      *
207      * @return the current color filter, or {@code null} if none set
208      */
getColorFilter(@onNull Drawable drawable)209     public static ColorFilter getColorFilter(@NonNull Drawable drawable) {
210         if (Build.VERSION.SDK_INT >= 21) {
211             return drawable.getColorFilter();
212         } else {
213             return null;
214         }
215     }
216 
217     /**
218      * Removes the color filter from the given drawable.
219      */
clearColorFilter(@onNull Drawable drawable)220     public static void clearColorFilter(@NonNull Drawable drawable) {
221         if (Build.VERSION.SDK_INT >= 23) {
222             // We can use clearColorFilter() safely on M+
223             drawable.clearColorFilter();
224         } else if (Build.VERSION.SDK_INT >= 21) {
225             drawable.clearColorFilter();
226 
227             // API 21 + 22 have an issue where clearing a color filter on a DrawableContainer
228             // will not propagate to all of its children. To workaround this we unwrap the drawable
229             // to find any DrawableContainers, and then unwrap those to clear the filter on its
230             // children manually
231             if (drawable instanceof InsetDrawable) {
232                 clearColorFilter(((InsetDrawable) drawable).getDrawable());
233             } else if (drawable instanceof WrappedDrawable) {
234                 clearColorFilter(((WrappedDrawable) drawable).getWrappedDrawable());
235             } else if (drawable instanceof DrawableContainer) {
236                 final DrawableContainer container = (DrawableContainer) drawable;
237                 final DrawableContainer.DrawableContainerState state =
238                         (DrawableContainer.DrawableContainerState) container.getConstantState();
239                 if (state != null) {
240                     Drawable child;
241                     for (int i = 0, count = state.getChildCount(); i < count; i++) {
242                         child = state.getChild(i);
243                         if (child != null) {
244                             clearColorFilter(child);
245                         }
246                     }
247                 }
248             }
249         } else {
250             drawable.clearColorFilter();
251         }
252     }
253 
254     /**
255      * Inflate this Drawable from an XML resource optionally styled by a theme.
256      *
257      * @param res Resources used to resolve attribute values
258      * @param parser XML parser from which to inflate this Drawable
259      * @param attrs Base set of attribute values
260      * @param theme Theme to apply, may be null
261      * @throws XmlPullParserException
262      * @throws IOException
263      */
inflate(@onNull Drawable drawable, @NonNull Resources res, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Resources.Theme theme)264     public static void inflate(@NonNull Drawable drawable, @NonNull Resources res,
265             @NonNull XmlPullParser parser, @NonNull AttributeSet attrs,
266             @Nullable Resources.Theme theme)
267             throws XmlPullParserException, IOException {
268         if (Build.VERSION.SDK_INT >= 21) {
269             drawable.inflate(res, parser, attrs, theme);
270         } else {
271             drawable.inflate(res, parser, attrs);
272         }
273     }
274 
275     /**
276      * Potentially wrap {@code drawable} so that it may be used for tinting across the
277      * different API levels, via the tinting methods in this class.
278      *
279      * <p>If the given drawable is wrapped, we will copy over certain state over to the wrapped
280      * drawable, such as its bounds, level, visibility and state.</p>
281      *
282      * <p>You must use the result of this call. If the given drawable is being used by a view
283      * (as its background for instance), you must replace the original drawable with
284      * the result of this call:</p>
285      *
286      * <pre>
287      * Drawable bg = DrawableCompat.wrap(view.getBackground());
288      * // Need to set the background with the wrapped drawable
289      * view.setBackground(bg);
290      *
291      * // You can now tint the drawable
292      * DrawableCompat.setTint(bg, ...);
293      * </pre>
294      *
295      * <p>If you need to get hold of the original {@link android.graphics.drawable.Drawable} again,
296      * you can use the value returned from {@link #unwrap(Drawable)}.</p>
297      *
298      * @param drawable The Drawable to process
299      * @return A drawable capable of being tinted across all API levels.
300      *
301      * @see #setTint(Drawable, int)
302      * @see #setTintList(Drawable, ColorStateList)
303      * @see #setTintMode(Drawable, PorterDuff.Mode)
304      * @see #unwrap(Drawable)
305      */
wrap(@onNull Drawable drawable)306     public static Drawable wrap(@NonNull Drawable drawable) {
307         if (Build.VERSION.SDK_INT >= 23) {
308             return drawable;
309         } else if (Build.VERSION.SDK_INT >= 21) {
310             if (!(drawable instanceof TintAwareDrawable)) {
311                 return new WrappedDrawableApi21(drawable);
312             }
313             return drawable;
314         } else {
315             if (!(drawable instanceof TintAwareDrawable)) {
316                 return new WrappedDrawableApi14(drawable);
317             }
318             return drawable;
319         }
320     }
321 
322     /**
323      * Unwrap {@code drawable} if it is the result of a call to {@link #wrap(Drawable)}. If
324      * the {@code drawable} is not the result of a call to {@link #wrap(Drawable)} then
325      * {@code drawable} is returned as-is.
326      *
327      * @param drawable The drawable to unwrap
328      * @return the unwrapped {@link Drawable} or {@code drawable} if it hasn't been wrapped.
329      *
330      * @see #wrap(Drawable)
331      */
332     @SuppressWarnings("TypeParameterUnusedInFormals")
unwrap(@onNull Drawable drawable)333     public static <T extends Drawable> T unwrap(@NonNull Drawable drawable) {
334         if (drawable instanceof WrappedDrawable) {
335             return (T) ((WrappedDrawable) drawable).getWrappedDrawable();
336         }
337         return (T) drawable;
338     }
339 
340     /**
341      * Set the layout direction for this drawable. Should be a resolved
342      * layout direction, as the Drawable has no capacity to do the resolution on
343      * its own.
344      *
345      * @param layoutDirection the resolved layout direction for the drawable,
346      *                        either {@link ViewCompat#LAYOUT_DIRECTION_LTR}
347      *                        or {@link ViewCompat#LAYOUT_DIRECTION_RTL}
348      * @return {@code true} if the layout direction change has caused the
349      *         appearance of the drawable to change such that it needs to be
350      *         re-drawn, {@code false} otherwise
351      * @see #getLayoutDirection(Drawable)
352      */
setLayoutDirection(@onNull Drawable drawable, int layoutDirection)353     public static boolean setLayoutDirection(@NonNull Drawable drawable, int layoutDirection) {
354         if (Build.VERSION.SDK_INT >= 23) {
355             return drawable.setLayoutDirection(layoutDirection);
356         } else if (Build.VERSION.SDK_INT >= 17) {
357             if (!sSetLayoutDirectionMethodFetched) {
358                 try {
359                     sSetLayoutDirectionMethod =
360                             Drawable.class.getDeclaredMethod("setLayoutDirection", int.class);
361                     sSetLayoutDirectionMethod.setAccessible(true);
362                 } catch (NoSuchMethodException e) {
363                     Log.i(TAG, "Failed to retrieve setLayoutDirection(int) method", e);
364                 }
365                 sSetLayoutDirectionMethodFetched = true;
366             }
367 
368             if (sSetLayoutDirectionMethod != null) {
369                 try {
370                     sSetLayoutDirectionMethod.invoke(drawable, layoutDirection);
371                     return true;
372                 } catch (Exception e) {
373                     Log.i(TAG, "Failed to invoke setLayoutDirection(int) via reflection", e);
374                     sSetLayoutDirectionMethod = null;
375                 }
376             }
377             return false;
378         } else {
379             return false;
380         }
381     }
382 
383     /**
384      * Returns the resolved layout direction for this Drawable.
385      *
386      * @return One of {@link ViewCompat#LAYOUT_DIRECTION_LTR},
387      *         {@link ViewCompat#LAYOUT_DIRECTION_RTL}
388      * @see #setLayoutDirection(Drawable, int)
389      */
getLayoutDirection(@onNull Drawable drawable)390     public static int getLayoutDirection(@NonNull Drawable drawable) {
391         if (Build.VERSION.SDK_INT >= 23) {
392             return drawable.getLayoutDirection();
393         } else if (Build.VERSION.SDK_INT >= 17) {
394             if (!sGetLayoutDirectionMethodFetched) {
395                 try {
396                     sGetLayoutDirectionMethod =
397                             Drawable.class.getDeclaredMethod("getLayoutDirection");
398                     sGetLayoutDirectionMethod.setAccessible(true);
399                 } catch (NoSuchMethodException e) {
400                     Log.i(TAG, "Failed to retrieve getLayoutDirection() method", e);
401                 }
402                 sGetLayoutDirectionMethodFetched = true;
403             }
404 
405             if (sGetLayoutDirectionMethod != null) {
406                 try {
407                     return (int) sGetLayoutDirectionMethod.invoke(drawable);
408                 } catch (Exception e) {
409                     Log.i(TAG, "Failed to invoke getLayoutDirection() via reflection", e);
410                     sGetLayoutDirectionMethod = null;
411                 }
412             }
413             return ViewCompat.LAYOUT_DIRECTION_LTR;
414         } else {
415             return ViewCompat.LAYOUT_DIRECTION_LTR;
416         }
417     }
418 
DrawableCompat()419     private DrawableCompat() {}
420 }
421