1 /* 2 * Copyright (C) 2014 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 17 package android.support.v7.widget; 18 19 import android.graphics.PorterDuff; 20 import android.graphics.Rect; 21 import android.graphics.drawable.Drawable; 22 import android.graphics.drawable.DrawableContainer; 23 import android.graphics.drawable.GradientDrawable; 24 import android.graphics.drawable.InsetDrawable; 25 import android.graphics.drawable.LayerDrawable; 26 import android.graphics.drawable.ScaleDrawable; 27 import android.graphics.drawable.StateListDrawable; 28 import android.os.Build; 29 import android.support.annotation.NonNull; 30 import android.support.v4.graphics.drawable.DrawableCompat; 31 import android.util.Log; 32 33 import java.lang.reflect.Field; 34 import java.lang.reflect.Method; 35 36 /** @hide */ 37 public class DrawableUtils { 38 39 private static final String TAG = "DrawableUtils"; 40 41 public static final Rect INSETS_NONE = new Rect(); 42 private static Class<?> sInsetsClazz; 43 44 private static final String VECTOR_DRAWABLE_CLAZZ_NAME 45 = "android.graphics.drawable.VectorDrawable"; 46 47 static { 48 if (Build.VERSION.SDK_INT >= 18) { 49 try { 50 sInsetsClazz = Class.forName("android.graphics.Insets"); 51 } catch (ClassNotFoundException e) { 52 // Oh well... 53 } 54 } 55 } 56 DrawableUtils()57 private DrawableUtils() {} 58 59 /** 60 * Allows us to get the optical insets for a {@link Drawable}. Since this is hidden we need to 61 * use reflection. Since the {@code Insets} class is hidden also, we return a Rect instead. 62 */ getOpticalBounds(Drawable drawable)63 public static Rect getOpticalBounds(Drawable drawable) { 64 if (sInsetsClazz != null) { 65 try { 66 // If the Drawable is wrapped, we need to manually unwrap it and process 67 // the wrapped drawable. 68 drawable = DrawableCompat.unwrap(drawable); 69 70 final Method getOpticalInsetsMethod = drawable.getClass() 71 .getMethod("getOpticalInsets"); 72 final Object insets = getOpticalInsetsMethod.invoke(drawable); 73 74 if (insets != null) { 75 // If the drawable has some optical insets, let's copy them into a Rect 76 final Rect result = new Rect(); 77 78 for (Field field : sInsetsClazz.getFields()) { 79 switch (field.getName()) { 80 case "left": 81 result.left = field.getInt(insets); 82 break; 83 case "top": 84 result.top = field.getInt(insets); 85 break; 86 case "right": 87 result.right = field.getInt(insets); 88 break; 89 case "bottom": 90 result.bottom = field.getInt(insets); 91 break; 92 } 93 } 94 return result; 95 } 96 } catch (Exception e) { 97 // Eugh, we hit some kind of reflection issue... 98 Log.e(TAG, "Couldn't obtain the optical insets. Ignoring."); 99 } 100 } 101 102 // If we reach here, either we're running on a device pre-v18, the Drawable didn't have 103 // any optical insets, or a reflection issue, so we'll just return an empty rect 104 return INSETS_NONE; 105 } 106 107 /** 108 * Attempt the fix any issues in the given drawable, usually caused by platform bugs in the 109 * implementation. This method should be call after retrieval from 110 * {@link android.content.res.Resources} or a {@link android.content.res.TypedArray}. 111 */ fixDrawable(@onNull final Drawable drawable)112 static void fixDrawable(@NonNull final Drawable drawable) { 113 if (Build.VERSION.SDK_INT == 21 114 && VECTOR_DRAWABLE_CLAZZ_NAME.equals(drawable.getClass().getName())) { 115 fixVectorDrawableTinting(drawable); 116 } 117 } 118 119 /** 120 * Some drawable implementations have problems with mutation. This method returns false if 121 * there is a known issue in the given drawable's implementation. 122 */ canSafelyMutateDrawable(@onNull Drawable drawable)123 public static boolean canSafelyMutateDrawable(@NonNull Drawable drawable) { 124 if (Build.VERSION.SDK_INT < 8 && drawable instanceof StateListDrawable) { 125 // StateListDrawable has a bug in mutate() on API 7 126 return false; 127 } else if (Build.VERSION.SDK_INT < 15 && drawable instanceof InsetDrawable) { 128 return false; 129 } else if (Build.VERSION.SDK_INT < 15 && drawable instanceof GradientDrawable) { 130 // GradientDrawable has a bug pre-ICS which results in mutate() resulting 131 // in loss of color 132 return false; 133 } else if (Build.VERSION.SDK_INT < 17 && drawable instanceof LayerDrawable) { 134 return false; 135 } 136 137 if (drawable instanceof DrawableContainer) { 138 // If we have a DrawableContainer, let's traverse it's child array 139 final Drawable.ConstantState state = drawable.getConstantState(); 140 if (state instanceof DrawableContainer.DrawableContainerState) { 141 final DrawableContainer.DrawableContainerState containerState = 142 (DrawableContainer.DrawableContainerState) state; 143 for (final Drawable child : containerState.getChildren()) { 144 if (!canSafelyMutateDrawable(child)) { 145 return false; 146 } 147 } 148 } 149 } else if (drawable instanceof android.support.v4.graphics.drawable.DrawableWrapper) { 150 return canSafelyMutateDrawable( 151 ((android.support.v4.graphics.drawable.DrawableWrapper) drawable) 152 .getWrappedDrawable()); 153 } else if (drawable instanceof android.support.v7.graphics.drawable.DrawableWrapper) { 154 return canSafelyMutateDrawable( 155 ((android.support.v7.graphics.drawable.DrawableWrapper) drawable) 156 .getWrappedDrawable()); 157 } else if (drawable instanceof ScaleDrawable) { 158 return canSafelyMutateDrawable(((ScaleDrawable) drawable).getDrawable()); 159 } 160 161 return true; 162 } 163 164 /** 165 * VectorDrawable has an issue on API 21 where it sometimes doesn't create its tint filter. 166 * Fixed by toggling it's state to force a filter creation. 167 */ fixVectorDrawableTinting(final Drawable drawable)168 private static void fixVectorDrawableTinting(final Drawable drawable) { 169 final int[] originalState = drawable.getState(); 170 if (originalState == null || originalState.length == 0) { 171 // The drawable doesn't have a state, so set it to be checked 172 drawable.setState(ThemeUtils.CHECKED_STATE_SET); 173 } else { 174 // Else the drawable does have a state, so clear it 175 drawable.setState(ThemeUtils.EMPTY_STATE_SET); 176 } 177 // Now set the original state 178 drawable.setState(originalState); 179 } 180 parseTintMode(int value, PorterDuff.Mode defaultMode)181 static PorterDuff.Mode parseTintMode(int value, PorterDuff.Mode defaultMode) { 182 switch (value) { 183 case 3: return PorterDuff.Mode.SRC_OVER; 184 case 5: return PorterDuff.Mode.SRC_IN; 185 case 9: return PorterDuff.Mode.SRC_ATOP; 186 case 14: return PorterDuff.Mode.MULTIPLY; 187 case 15: return PorterDuff.Mode.SCREEN; 188 case 16: return Build.VERSION.SDK_INT >= 11 189 ? PorterDuff.Mode.valueOf("ADD") 190 : defaultMode; 191 default: return defaultMode; 192 } 193 } 194 195 } 196