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