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