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