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