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