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 androidx.core.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.os.Build;
27import android.util.AttributeSet;
28import android.util.Log;
29
30import androidx.annotation.ColorInt;
31import androidx.annotation.NonNull;
32import androidx.annotation.Nullable;
33import androidx.core.view.ViewCompat;
34
35import org.xmlpull.v1.XmlPullParser;
36import org.xmlpull.v1.XmlPullParserException;
37
38import java.io.IOException;
39import java.lang.reflect.Method;
40
41/**
42 * Helper for accessing features in {@link android.graphics.drawable.Drawable}.
43 */
44public 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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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")
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     */
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     */
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
419    private DrawableCompat() {}
420}
421