1414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes/*
2414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes * Copyright (C) 2014 The Android Open Source Project
3414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes *
4414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes * Licensed under the Apache License, Version 2.0 (the "License");
5414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes * you may not use this file except in compliance with the License.
6414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes * You may obtain a copy of the License at
7414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes *
8414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes *      http://www.apache.org/licenses/LICENSE-2.0
9414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes *
10414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes * Unless required by applicable law or agreed to in writing, software
11414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes * distributed under the License is distributed on an "AS IS" BASIS,
12414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes * See the License for the specific language governing permissions and
14414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes * limitations under the License.
15414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes */
16414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes
1766698bb15ba0f873aa1c2290cc50d6bb839a474aChris Banespackage android.support.v7.widget;
18414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes
19aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banesimport android.graphics.PorterDuff;
20414f52397a88d52a783a31d4c098bc3bec632b8dChris Banesimport android.graphics.Rect;
21414f52397a88d52a783a31d4c098bc3bec632b8dChris Banesimport android.graphics.drawable.Drawable;
22aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banesimport android.graphics.drawable.DrawableContainer;
23aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banesimport android.graphics.drawable.GradientDrawable;
24aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banesimport android.graphics.drawable.InsetDrawable;
25aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banesimport android.graphics.drawable.LayerDrawable;
26a7331ff81580eafb62c67a083e53ca82658c556eChris Banesimport android.graphics.drawable.ScaleDrawable;
27aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banesimport android.graphics.drawable.StateListDrawable;
28414f52397a88d52a783a31d4c098bc3bec632b8dChris Banesimport android.os.Build;
29aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banesimport android.support.annotation.NonNull;
30c39d9c75590eca86a5e7e32a8824ba04a0d42e9bAlan Viveretteimport android.support.annotation.RestrictTo;
317e82b99953680915596eaf0eb35927388e574ca8Chris Banesimport android.support.v4.graphics.drawable.DrawableCompat;
32414f52397a88d52a783a31d4c098bc3bec632b8dChris Banesimport android.util.Log;
33414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes
34414f52397a88d52a783a31d4c098bc3bec632b8dChris Banesimport java.lang.reflect.Field;
35414f52397a88d52a783a31d4c098bc3bec632b8dChris Banesimport java.lang.reflect.Method;
36414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes
37c39d9c75590eca86a5e7e32a8824ba04a0d42e9bAlan Viveretteimport static android.support.annotation.RestrictTo.Scope.GROUP_ID;
38c39d9c75590eca86a5e7e32a8824ba04a0d42e9bAlan Viverette
390f6a807ceaa96930e57e55826ddaae5eab692aa8Kirill Grouchnikov/** @hide */
40c39d9c75590eca86a5e7e32a8824ba04a0d42e9bAlan Viverette@RestrictTo(GROUP_ID)
410f6a807ceaa96930e57e55826ddaae5eab692aa8Kirill Grouchnikovpublic class DrawableUtils {
42414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes
43414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes    private static final String TAG = "DrawableUtils";
44414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes
45414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes    public static final Rect INSETS_NONE = new Rect();
46414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes    private static Class<?> sInsetsClazz;
47414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes
48aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes    private static final String VECTOR_DRAWABLE_CLAZZ_NAME
49aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            = "android.graphics.drawable.VectorDrawable";
50aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes
51414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes    static {
52414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes        if (Build.VERSION.SDK_INT >= 18) {
53414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes            try {
54414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                sInsetsClazz = Class.forName("android.graphics.Insets");
55414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes            } catch (ClassNotFoundException e) {
56414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                // Oh well...
57414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes            }
58414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes        }
59414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes    }
60414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes
61414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes    private DrawableUtils() {}
62414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes
63414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes    /**
64414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes     * Allows us to get the optical insets for a {@link Drawable}. Since this is hidden we need to
65414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes     * use reflection. Since the {@code Insets} class is hidden also, we return a Rect instead.
66414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes     */
67414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes    public static Rect getOpticalBounds(Drawable drawable) {
68414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes        if (sInsetsClazz != null) {
69414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes            try {
707e82b99953680915596eaf0eb35927388e574ca8Chris Banes                // If the Drawable is wrapped, we need to manually unwrap it and process
717e82b99953680915596eaf0eb35927388e574ca8Chris Banes                // the wrapped drawable.
727e82b99953680915596eaf0eb35927388e574ca8Chris Banes                drawable = DrawableCompat.unwrap(drawable);
73414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes
74414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                final Method getOpticalInsetsMethod = drawable.getClass()
75414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                        .getMethod("getOpticalInsets");
76414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                final Object insets = getOpticalInsetsMethod.invoke(drawable);
77414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes
78414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                if (insets != null) {
79414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                    // If the drawable has some optical insets, let's copy them into a Rect
80414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                    final Rect result = new Rect();
81414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes
82414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                    for (Field field : sInsetsClazz.getFields()) {
83414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                        switch (field.getName()) {
84414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                            case "left":
85414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                               result.left = field.getInt(insets);
86414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                                break;
87414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                            case "top":
88414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                                result.top = field.getInt(insets);
89414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                                break;
90414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                            case "right":
91414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                                result.right = field.getInt(insets);
92414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                                break;
93414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                            case "bottom":
94414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                                result.bottom = field.getInt(insets);
95414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                                break;
96414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                        }
97414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                    }
98414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                    return result;
99414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                }
100414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes            } catch (Exception e) {
101414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                // Eugh, we hit some kind of reflection issue...
102414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes                Log.e(TAG, "Couldn't obtain the optical insets. Ignoring.");
103414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes            }
104414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes        }
105414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes
106414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes        // If we reach here, either we're running on a device pre-v18, the Drawable didn't have
107414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes        // any optical insets, or a reflection issue, so we'll just return an empty rect
108414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes        return INSETS_NONE;
109414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes    }
110414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes
111aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes    /**
112aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes     * Attempt the fix any issues in the given drawable, usually caused by platform bugs in the
113aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes     * implementation. This method should be call after retrieval from
114aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes     * {@link android.content.res.Resources} or a {@link android.content.res.TypedArray}.
115aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes     */
116aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes    static void fixDrawable(@NonNull final Drawable drawable) {
117aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes        if (Build.VERSION.SDK_INT == 21
118aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes                && VECTOR_DRAWABLE_CLAZZ_NAME.equals(drawable.getClass().getName())) {
119aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            fixVectorDrawableTinting(drawable);
120aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes        }
121aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes    }
122aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes
123aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes    /**
124aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes     * Some drawable implementations have problems with mutation. This method returns false if
125aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes     * there is a known issue in the given drawable's implementation.
126aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes     */
1270f6a807ceaa96930e57e55826ddaae5eab692aa8Kirill Grouchnikov    public static boolean canSafelyMutateDrawable(@NonNull Drawable drawable) {
12864dbe1d454f1190b3cd8426d09b9119949a10709Kirill Grouchnikov        if (Build.VERSION.SDK_INT < 15 && drawable instanceof InsetDrawable) {
1294237288e940e6c9c0ec8719dffd6b63fedd9056aChris Banes            return false;
1304237288e940e6c9c0ec8719dffd6b63fedd9056aChris Banes        }  else if (Build.VERSION.SDK_INT < 15 && drawable instanceof GradientDrawable) {
131aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            // GradientDrawable has a bug pre-ICS which results in mutate() resulting
132aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            // in loss of color
1334237288e940e6c9c0ec8719dffd6b63fedd9056aChris Banes            return false;
1344237288e940e6c9c0ec8719dffd6b63fedd9056aChris Banes        } else if (Build.VERSION.SDK_INT < 17 && drawable instanceof LayerDrawable) {
1354237288e940e6c9c0ec8719dffd6b63fedd9056aChris Banes            return false;
1364237288e940e6c9c0ec8719dffd6b63fedd9056aChris Banes        }
1374237288e940e6c9c0ec8719dffd6b63fedd9056aChris Banes
1384237288e940e6c9c0ec8719dffd6b63fedd9056aChris Banes        if (drawable instanceof DrawableContainer) {
139aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            // If we have a DrawableContainer, let's traverse it's child array
140aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            final Drawable.ConstantState state = drawable.getConstantState();
141aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            if (state instanceof DrawableContainer.DrawableContainerState) {
142aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes                final DrawableContainer.DrawableContainerState containerState =
143aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes                        (DrawableContainer.DrawableContainerState) state;
1444237288e940e6c9c0ec8719dffd6b63fedd9056aChris Banes                for (final Drawable child : containerState.getChildren()) {
145aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes                    if (!canSafelyMutateDrawable(child)) {
146aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes                        return false;
147aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes                    }
148aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes                }
149aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            }
150547c54f394ad16eb8d7ded400e2d2cf6e7256bb8Chris Banes        } else if (drawable instanceof android.support.v4.graphics.drawable.DrawableWrapper) {
151547c54f394ad16eb8d7ded400e2d2cf6e7256bb8Chris Banes            return canSafelyMutateDrawable(
152547c54f394ad16eb8d7ded400e2d2cf6e7256bb8Chris Banes                    ((android.support.v4.graphics.drawable.DrawableWrapper) drawable)
153547c54f394ad16eb8d7ded400e2d2cf6e7256bb8Chris Banes                            .getWrappedDrawable());
154547c54f394ad16eb8d7ded400e2d2cf6e7256bb8Chris Banes        } else if (drawable instanceof android.support.v7.graphics.drawable.DrawableWrapper) {
155547c54f394ad16eb8d7ded400e2d2cf6e7256bb8Chris Banes            return canSafelyMutateDrawable(
156547c54f394ad16eb8d7ded400e2d2cf6e7256bb8Chris Banes                    ((android.support.v7.graphics.drawable.DrawableWrapper) drawable)
157547c54f394ad16eb8d7ded400e2d2cf6e7256bb8Chris Banes                            .getWrappedDrawable());
158a7331ff81580eafb62c67a083e53ca82658c556eChris Banes        } else if (drawable instanceof ScaleDrawable) {
159a7331ff81580eafb62c67a083e53ca82658c556eChris Banes            return canSafelyMutateDrawable(((ScaleDrawable) drawable).getDrawable());
160aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes        }
1614237288e940e6c9c0ec8719dffd6b63fedd9056aChris Banes
162aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes        return true;
163aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes    }
164aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes
165aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes    /**
166aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes     * VectorDrawable has an issue on API 21 where it sometimes doesn't create its tint filter.
167aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes     * Fixed by toggling it's state to force a filter creation.
168aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes     */
169aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes    private static void fixVectorDrawableTinting(final Drawable drawable) {
170aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes        final int[] originalState = drawable.getState();
171aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes        if (originalState == null || originalState.length == 0) {
172aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            // The drawable doesn't have a state, so set it to be checked
173aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            drawable.setState(ThemeUtils.CHECKED_STATE_SET);
174aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes        } else {
175aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            // Else the drawable does have a state, so clear it
176aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            drawable.setState(ThemeUtils.EMPTY_STATE_SET);
177aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes        }
178aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes        // Now set the original state
179aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes        drawable.setState(originalState);
180aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes    }
181aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes
182aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes    static PorterDuff.Mode parseTintMode(int value, PorterDuff.Mode defaultMode) {
183aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes        switch (value) {
184aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            case 3: return PorterDuff.Mode.SRC_OVER;
185aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            case 5: return PorterDuff.Mode.SRC_IN;
186aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            case 9: return PorterDuff.Mode.SRC_ATOP;
187aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            case 14: return PorterDuff.Mode.MULTIPLY;
188aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            case 15: return PorterDuff.Mode.SCREEN;
189aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            case 16: return Build.VERSION.SDK_INT >= 11
190aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes                    ? PorterDuff.Mode.valueOf("ADD")
191aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes                    : defaultMode;
192aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes            default: return defaultMode;
193aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes        }
194aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes    }
195aaa85b7d563d27fdf10048dd619a317451477ad5Chris Banes
196414f52397a88d52a783a31d4c098bc3bec632b8dChris Banes}
197