• 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 android.support.v4.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.support.annotation.ColorInt;
28 import android.support.annotation.NonNull;
29 import android.support.annotation.Nullable;
30 import android.support.annotation.RequiresApi;
31 import android.support.v4.view.ViewCompat;
32 import android.util.AttributeSet;
33 import android.util.Log;
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  * introduced after API level 4 in a backwards compatible fashion.
44  */
45 public final class DrawableCompat {
46     /**
47      * Interface implementation that doesn't use anything about v4 APIs.
48      */
49     static class DrawableCompatBaseImpl {
jumpToCurrentState(Drawable drawable)50         public void jumpToCurrentState(Drawable drawable) {
51             drawable.jumpToCurrentState();
52         }
53 
setAutoMirrored(Drawable drawable, boolean mirrored)54         public void setAutoMirrored(Drawable drawable, boolean mirrored) {
55         }
56 
isAutoMirrored(Drawable drawable)57         public boolean isAutoMirrored(Drawable drawable) {
58             return false;
59         }
60 
setHotspot(Drawable drawable, float x, float y)61         public void setHotspot(Drawable drawable, float x, float y) {
62         }
63 
setHotspotBounds(Drawable drawable, int left, int top, int right, int bottom)64         public void setHotspotBounds(Drawable drawable, int left, int top, int right, int bottom) {
65         }
66 
setTint(Drawable drawable, int tint)67         public void setTint(Drawable drawable, int tint) {
68             if (drawable instanceof TintAwareDrawable) {
69                 ((TintAwareDrawable) drawable).setTint(tint);
70             }
71         }
72 
setTintList(Drawable drawable, ColorStateList tint)73         public void setTintList(Drawable drawable, ColorStateList tint) {
74             if (drawable instanceof TintAwareDrawable) {
75                 ((TintAwareDrawable) drawable).setTintList(tint);
76             }
77         }
78 
setTintMode(Drawable drawable, PorterDuff.Mode tintMode)79         public void setTintMode(Drawable drawable, PorterDuff.Mode tintMode) {
80             if (drawable instanceof TintAwareDrawable) {
81                 ((TintAwareDrawable) drawable).setTintMode(tintMode);
82             }
83         }
84 
wrap(Drawable drawable)85         public Drawable wrap(Drawable drawable) {
86             if (!(drawable instanceof TintAwareDrawable)) {
87                 return new DrawableWrapperApi14(drawable);
88             }
89             return drawable;
90         }
91 
setLayoutDirection(Drawable drawable, int layoutDirection)92         public boolean setLayoutDirection(Drawable drawable, int layoutDirection) {
93             // No op for API < 23
94             return false;
95         }
96 
getLayoutDirection(Drawable drawable)97         public int getLayoutDirection(Drawable drawable) {
98             return ViewCompat.LAYOUT_DIRECTION_LTR;
99         }
100 
getAlpha(Drawable drawable)101         public int getAlpha(Drawable drawable) {
102             return 0;
103         }
104 
applyTheme(Drawable drawable, Resources.Theme t)105         public void applyTheme(Drawable drawable, Resources.Theme t) {
106         }
107 
canApplyTheme(Drawable drawable)108         public boolean canApplyTheme(Drawable drawable) {
109             return false;
110         }
111 
getColorFilter(Drawable drawable)112         public ColorFilter getColorFilter(Drawable drawable) {
113             return null;
114         }
115 
clearColorFilter(Drawable drawable)116         public void clearColorFilter(Drawable drawable) {
117             drawable.clearColorFilter();
118         }
119 
inflate(Drawable drawable, Resources res, XmlPullParser parser, AttributeSet attrs, Resources.Theme t)120         public void inflate(Drawable drawable, Resources res, XmlPullParser parser,
121                             AttributeSet attrs, Resources.Theme t)
122                 throws IOException, XmlPullParserException {
123             drawable.inflate(res, parser, attrs);
124         }
125     }
126 
127     @RequiresApi(17)
128     static class DrawableCompatApi17Impl extends DrawableCompatBaseImpl {
129         private static final String TAG = "DrawableCompatApi17";
130 
131         private static Method sSetLayoutDirectionMethod;
132         private static boolean sSetLayoutDirectionMethodFetched;
133 
134         private static Method sGetLayoutDirectionMethod;
135         private static boolean sGetLayoutDirectionMethodFetched;
136 
137         @Override
setLayoutDirection(Drawable drawable, int layoutDirection)138         public boolean setLayoutDirection(Drawable drawable, int layoutDirection) {
139             if (!sSetLayoutDirectionMethodFetched) {
140                 try {
141                     sSetLayoutDirectionMethod =
142                             Drawable.class.getDeclaredMethod("setLayoutDirection", int.class);
143                     sSetLayoutDirectionMethod.setAccessible(true);
144                 } catch (NoSuchMethodException e) {
145                     Log.i(TAG, "Failed to retrieve setLayoutDirection(int) method", e);
146                 }
147                 sSetLayoutDirectionMethodFetched = true;
148             }
149 
150             if (sSetLayoutDirectionMethod != null) {
151                 try {
152                     sSetLayoutDirectionMethod.invoke(drawable, layoutDirection);
153                     return true;
154                 } catch (Exception e) {
155                     Log.i(TAG, "Failed to invoke setLayoutDirection(int) via reflection", e);
156                     sSetLayoutDirectionMethod = null;
157                 }
158             }
159             return false;
160         }
161 
162         @Override
getLayoutDirection(Drawable drawable)163         public int getLayoutDirection(Drawable drawable) {
164             if (!sGetLayoutDirectionMethodFetched) {
165                 try {
166                     sGetLayoutDirectionMethod = Drawable.class.getDeclaredMethod("getLayoutDirection");
167                     sGetLayoutDirectionMethod.setAccessible(true);
168                 } catch (NoSuchMethodException e) {
169                     Log.i(TAG, "Failed to retrieve getLayoutDirection() method", e);
170                 }
171                 sGetLayoutDirectionMethodFetched = true;
172             }
173 
174             if (sGetLayoutDirectionMethod != null) {
175                 try {
176                     return (int) sGetLayoutDirectionMethod.invoke(drawable);
177                 } catch (Exception e) {
178                     Log.i(TAG, "Failed to invoke getLayoutDirection() via reflection", e);
179                     sGetLayoutDirectionMethod = null;
180                 }
181             }
182             return ViewCompat.LAYOUT_DIRECTION_LTR;
183         }
184     }
185 
186     /**
187      * Interface implementation for devices with at least KitKat APIs.
188      */
189     @RequiresApi(19)
190     static class DrawableCompatApi19Impl extends DrawableCompatApi17Impl {
191         @Override
setAutoMirrored(Drawable drawable, boolean mirrored)192         public void setAutoMirrored(Drawable drawable, boolean mirrored) {
193             drawable.setAutoMirrored(mirrored);
194         }
195 
196         @Override
isAutoMirrored(Drawable drawable)197         public boolean isAutoMirrored(Drawable drawable) {
198             return drawable.isAutoMirrored();
199         }
200 
201         @Override
wrap(Drawable drawable)202         public Drawable wrap(Drawable drawable) {
203             if (!(drawable instanceof TintAwareDrawable)) {
204                 return new DrawableWrapperApi19(drawable);
205             }
206             return drawable;
207         }
208 
209         @Override
getAlpha(Drawable drawable)210         public int getAlpha(Drawable drawable) {
211             return drawable.getAlpha();
212         }
213     }
214 
215     /**
216      * Interface implementation for devices with at least L APIs.
217      */
218     @RequiresApi(21)
219     static class DrawableCompatApi21Impl extends DrawableCompatApi19Impl {
220         @Override
setHotspot(Drawable drawable, float x, float y)221         public void setHotspot(Drawable drawable, float x, float y) {
222             drawable.setHotspot(x, y);
223         }
224 
225         @Override
setHotspotBounds(Drawable drawable, int left, int top, int right, int bottom)226         public void setHotspotBounds(Drawable drawable, int left, int top, int right, int bottom) {
227             drawable.setHotspotBounds(left, top, right, bottom);
228         }
229 
230         @Override
setTint(Drawable drawable, int tint)231         public void setTint(Drawable drawable, int tint) {
232             drawable.setTint(tint);
233         }
234 
235         @Override
setTintList(Drawable drawable, ColorStateList tint)236         public void setTintList(Drawable drawable, ColorStateList tint) {
237             drawable.setTintList(tint);
238         }
239 
240         @Override
setTintMode(Drawable drawable, PorterDuff.Mode tintMode)241         public void setTintMode(Drawable drawable, PorterDuff.Mode tintMode) {
242             drawable.setTintMode(tintMode);
243         }
244 
245         @Override
wrap(Drawable drawable)246         public Drawable wrap(Drawable drawable) {
247             if (!(drawable instanceof TintAwareDrawable)) {
248                 return new DrawableWrapperApi21(drawable);
249             }
250             return drawable;
251         }
252 
253         @Override
applyTheme(Drawable drawable, Resources.Theme t)254         public void applyTheme(Drawable drawable, Resources.Theme t) {
255             drawable.applyTheme(t);
256         }
257 
258         @Override
canApplyTheme(Drawable drawable)259         public boolean canApplyTheme(Drawable drawable) {
260             return drawable.canApplyTheme();
261         }
262 
263         @Override
getColorFilter(Drawable drawable)264         public ColorFilter getColorFilter(Drawable drawable) {
265             return drawable.getColorFilter();
266         }
267 
268         @Override
clearColorFilter(Drawable drawable)269         public void clearColorFilter(Drawable drawable) {
270             drawable.clearColorFilter();
271 
272             // API 21 + 22 have an issue where clearing a color filter on a DrawableContainer
273             // will not propagate to all of its children. To workaround this we unwrap the drawable
274             // to find any DrawableContainers, and then unwrap those to clear the filter on its
275             // children manually
276             if (drawable instanceof InsetDrawable) {
277                 clearColorFilter(((InsetDrawable) drawable).getDrawable());
278             } else if (drawable instanceof DrawableWrapper) {
279                 clearColorFilter(((DrawableWrapper) drawable).getWrappedDrawable());
280             } else if (drawable instanceof DrawableContainer) {
281                 final DrawableContainer container = (DrawableContainer) drawable;
282                 final DrawableContainer.DrawableContainerState state =
283                         (DrawableContainer.DrawableContainerState) container.getConstantState();
284                 if (state != null) {
285                     Drawable child;
286                     for (int i = 0, count = state.getChildCount(); i < count; i++) {
287                         child = state.getChild(i);
288                         if (child != null) {
289                             clearColorFilter(child);
290                         }
291                     }
292                 }
293             }
294         }
295 
296         @Override
inflate(Drawable drawable, Resources res, XmlPullParser parser, AttributeSet attrs, Resources.Theme t)297         public void inflate(Drawable drawable, Resources res, XmlPullParser parser,
298                             AttributeSet attrs, Resources.Theme t)
299                 throws IOException, XmlPullParserException {
300             drawable.inflate(res, parser, attrs, t);
301         }
302     }
303 
304     /**
305      * Interface implementation for devices with at least M APIs.
306      */
307     @RequiresApi(23)
308     static class DrawableCompatApi23Impl extends DrawableCompatApi21Impl {
309         @Override
setLayoutDirection(Drawable drawable, int layoutDirection)310         public boolean setLayoutDirection(Drawable drawable, int layoutDirection) {
311             return drawable.setLayoutDirection(layoutDirection);
312         }
313 
314         @Override
getLayoutDirection(Drawable drawable)315         public int getLayoutDirection(Drawable drawable) {
316             return drawable.getLayoutDirection();
317         }
318 
319         @Override
wrap(Drawable drawable)320         public Drawable wrap(Drawable drawable) {
321             // No need to wrap on M+
322             return drawable;
323         }
324 
325         @Override
clearColorFilter(Drawable drawable)326         public void clearColorFilter(Drawable drawable) {
327             // We can use clearColorFilter() safely on M+
328             drawable.clearColorFilter();
329         }
330     }
331 
332     /**
333      * Select the correct implementation to use for the current platform.
334      */
335     static final DrawableCompatBaseImpl IMPL;
336     static {
337         if (Build.VERSION.SDK_INT >= 23) {
338             IMPL = new DrawableCompatApi23Impl();
339         } else if (Build.VERSION.SDK_INT >= 21) {
340             IMPL = new DrawableCompatApi21Impl();
341         } else if (Build.VERSION.SDK_INT >= 19) {
342             IMPL = new DrawableCompatApi19Impl();
343         } else if (Build.VERSION.SDK_INT >= 17) {
344             IMPL = new DrawableCompatApi17Impl();
345         } else {
346             IMPL = new DrawableCompatBaseImpl();
347         }
348     }
349 
350     /**
351      * Call {@link Drawable#jumpToCurrentState() Drawable.jumpToCurrentState()}.
352      * <p>
353      * If running on a pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB}
354      * device this method does nothing.
355      *
356      * @param drawable The Drawable against which to invoke the method.
357      */
jumpToCurrentState(@onNull Drawable drawable)358     public static void jumpToCurrentState(@NonNull Drawable drawable) {
359         IMPL.jumpToCurrentState(drawable);
360     }
361 
362     /**
363      * Set whether this Drawable is automatically mirrored when its layout
364      * direction is RTL (right-to left). See
365      * {@link android.util.LayoutDirection}.
366      * <p>
367      * If running on a pre-{@link android.os.Build.VERSION_CODES#KITKAT} device
368      * this method does nothing.
369      *
370      * @param drawable The Drawable against which to invoke the method.
371      * @param mirrored Set to true if the Drawable should be mirrored, false if
372      *            not.
373      */
setAutoMirrored(@onNull Drawable drawable, boolean mirrored)374     public static void setAutoMirrored(@NonNull Drawable drawable, boolean mirrored) {
375         IMPL.setAutoMirrored(drawable, mirrored);
376     }
377 
378     /**
379      * Tells if this Drawable will be automatically mirrored when its layout
380      * direction is RTL right-to-left. See {@link android.util.LayoutDirection}.
381      * <p>
382      * If running on a pre-{@link android.os.Build.VERSION_CODES#KITKAT} device
383      * this method returns false.
384      *
385      * @param drawable The Drawable against which to invoke the method.
386      * @return boolean Returns true if this Drawable will be automatically
387      *         mirrored.
388      */
isAutoMirrored(@onNull Drawable drawable)389     public static boolean isAutoMirrored(@NonNull Drawable drawable) {
390         return IMPL.isAutoMirrored(drawable);
391     }
392 
393     /**
394      * Specifies the hotspot's location within the drawable.
395      *
396      * @param drawable The Drawable against which to invoke the method.
397      * @param x The X coordinate of the center of the hotspot
398      * @param y The Y coordinate of the center of the hotspot
399      */
setHotspot(@onNull Drawable drawable, float x, float y)400     public static void setHotspot(@NonNull Drawable drawable, float x, float y) {
401         IMPL.setHotspot(drawable, x, y);
402     }
403 
404     /**
405      * Sets the bounds to which the hotspot is constrained, if they should be
406      * different from the drawable bounds.
407      *
408      * @param drawable The Drawable against which to invoke the method.
409      */
setHotspotBounds(@onNull Drawable drawable, int left, int top, int right, int bottom)410     public static void setHotspotBounds(@NonNull Drawable drawable, int left, int top,
411             int right, int bottom) {
412         IMPL.setHotspotBounds(drawable, left, top, right, bottom);
413     }
414 
415     /**
416      * Specifies a tint for {@code drawable}.
417      *
418      * @param drawable The Drawable against which to invoke the method.
419      * @param tint     Color to use for tinting this drawable
420      */
setTint(@onNull Drawable drawable, @ColorInt int tint)421     public static void setTint(@NonNull Drawable drawable, @ColorInt int tint) {
422         IMPL.setTint(drawable, tint);
423     }
424 
425     /**
426      * Specifies a tint for {@code drawable} as a color state list.
427      *
428      * @param drawable The Drawable against which to invoke the method.
429      * @param tint     Color state list to use for tinting this drawable, or null to clear the tint
430      */
setTintList(@onNull Drawable drawable, @Nullable ColorStateList tint)431     public static void setTintList(@NonNull Drawable drawable, @Nullable ColorStateList tint) {
432         IMPL.setTintList(drawable, tint);
433     }
434 
435     /**
436      * Specifies a tint blending mode for {@code drawable}.
437      *
438      * @param drawable The Drawable against which to invoke the method.
439      * @param tintMode A Porter-Duff blending mode
440      */
setTintMode(@onNull Drawable drawable, @Nullable PorterDuff.Mode tintMode)441     public static void setTintMode(@NonNull Drawable drawable, @Nullable PorterDuff.Mode tintMode) {
442         IMPL.setTintMode(drawable, tintMode);
443     }
444 
445     /**
446      * Get the alpha value of the {@code drawable}.
447      * 0 means fully transparent, 255 means fully opaque.
448      *
449      * @param drawable The Drawable against which to invoke the method.
450      */
getAlpha(@onNull Drawable drawable)451     public static int getAlpha(@NonNull Drawable drawable) {
452         return IMPL.getAlpha(drawable);
453     }
454 
455     /**
456      * Applies the specified theme to this Drawable and its children.
457      */
applyTheme(@onNull Drawable drawable, @NonNull Resources.Theme t)458     public static void applyTheme(@NonNull Drawable drawable, @NonNull Resources.Theme t) {
459         IMPL.applyTheme(drawable, t);
460     }
461 
462     /**
463      * Whether a theme can be applied to this Drawable and its children.
464      */
canApplyTheme(@onNull Drawable drawable)465     public static boolean canApplyTheme(@NonNull Drawable drawable) {
466         return IMPL.canApplyTheme(drawable);
467     }
468 
469     /**
470      * Returns the current color filter, or {@code null} if none set.
471      *
472      * @return the current color filter, or {@code null} if none set
473      */
getColorFilter(@onNull Drawable drawable)474     public static ColorFilter getColorFilter(@NonNull Drawable drawable) {
475         return IMPL.getColorFilter(drawable);
476     }
477 
478     /**
479      * Removes the color filter from the given drawable.
480      */
clearColorFilter(@onNull Drawable drawable)481     public static void clearColorFilter(@NonNull Drawable drawable) {
482         IMPL.clearColorFilter(drawable);
483     }
484 
485     /**
486      * Inflate this Drawable from an XML resource optionally styled by a theme.
487      *
488      * @param res Resources used to resolve attribute values
489      * @param parser XML parser from which to inflate this Drawable
490      * @param attrs Base set of attribute values
491      * @param theme Theme to apply, may be null
492      * @throws XmlPullParserException
493      * @throws IOException
494      */
inflate(@onNull Drawable drawable, @NonNull Resources res, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Resources.Theme theme)495     public static void inflate(@NonNull Drawable drawable, @NonNull Resources res,
496             @NonNull XmlPullParser parser, @NonNull AttributeSet attrs,
497             @Nullable Resources.Theme theme)
498             throws XmlPullParserException, IOException {
499         IMPL.inflate(drawable, res, parser, attrs, theme);
500     }
501 
502     /**
503      * Potentially wrap {@code drawable} so that it may be used for tinting across the
504      * different API levels, via the tinting methods in this class.
505      *
506      * <p>If the given drawable is wrapped, we will copy over certain state over to the wrapped
507      * drawable, such as its bounds, level, visibility and state.</p>
508      *
509      * <p>You must use the result of this call. If the given drawable is being used by a view
510      * (as its background for instance), you must replace the original drawable with
511      * the result of this call:</p>
512      *
513      * <pre>
514      * Drawable bg = DrawableCompat.wrap(view.getBackground());
515      * // Need to set the background with the wrapped drawable
516      * view.setBackground(bg);
517      *
518      * // You can now tint the drawable
519      * DrawableCompat.setTint(bg, ...);
520      * </pre>
521      *
522      * <p>If you need to get hold of the original {@link android.graphics.drawable.Drawable} again,
523      * you can use the value returned from {@link #unwrap(Drawable)}.</p>
524      *
525      * @param drawable The Drawable to process
526      * @return A drawable capable of being tinted across all API levels.
527      *
528      * @see #setTint(Drawable, int)
529      * @see #setTintList(Drawable, ColorStateList)
530      * @see #setTintMode(Drawable, PorterDuff.Mode)
531      * @see #unwrap(Drawable)
532      */
wrap(@onNull Drawable drawable)533     public static Drawable wrap(@NonNull Drawable drawable) {
534         return IMPL.wrap(drawable);
535     }
536 
537     /**
538      * Unwrap {@code drawable} if it is the result of a call to {@link #wrap(Drawable)}. If
539      * the {@code drawable} is not the result of a call to {@link #wrap(Drawable)} then
540      * {@code drawable} is returned as-is.
541      *
542      * @param drawable The drawable to unwrap
543      * @return the unwrapped {@link Drawable} or {@code drawable} if it hasn't been wrapped.
544      *
545      * @see #wrap(Drawable)
546      */
547     @SuppressWarnings("TypeParameterUnusedInFormals")
unwrap(@onNull Drawable drawable)548     public static <T extends Drawable> T unwrap(@NonNull Drawable drawable) {
549         if (drawable instanceof DrawableWrapper) {
550             return (T) ((DrawableWrapper) drawable).getWrappedDrawable();
551         }
552         return (T) drawable;
553     }
554 
555     /**
556      * Set the layout direction for this drawable. Should be a resolved
557      * layout direction, as the Drawable has no capacity to do the resolution on
558      * its own.
559      *
560      * @param layoutDirection the resolved layout direction for the drawable,
561      *                        either {@link ViewCompat#LAYOUT_DIRECTION_LTR}
562      *                        or {@link ViewCompat#LAYOUT_DIRECTION_RTL}
563      * @return {@code true} if the layout direction change has caused the
564      *         appearance of the drawable to change such that it needs to be
565      *         re-drawn, {@code false} otherwise
566      * @see #getLayoutDirection(Drawable)
567      */
setLayoutDirection(@onNull Drawable drawable, int layoutDirection)568     public static boolean setLayoutDirection(@NonNull Drawable drawable, int layoutDirection) {
569         return IMPL.setLayoutDirection(drawable, layoutDirection);
570     }
571 
572     /**
573      * Returns the resolved layout direction for this Drawable.
574      *
575      * @return One of {@link ViewCompat#LAYOUT_DIRECTION_LTR},
576      *         {@link ViewCompat#LAYOUT_DIRECTION_RTL}
577      * @see #setLayoutDirection(Drawable, int)
578      */
getLayoutDirection(@onNull Drawable drawable)579     public static int getLayoutDirection(@NonNull Drawable drawable) {
580         return IMPL.getLayoutDirection(drawable);
581     }
582 
DrawableCompat()583     private DrawableCompat() {}
584 }
585