TintManager.java revision b37a31664b07243ca9e86c8dac58b9be6a417e8c
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.internal.widget; 18 19import android.content.Context; 20import android.content.res.ColorStateList; 21import android.content.res.Resources; 22import android.graphics.Color; 23import android.graphics.PorterDuff; 24import android.graphics.PorterDuffColorFilter; 25import android.graphics.drawable.Drawable; 26import android.support.v4.content.ContextCompat; 27import android.support.v4.util.LruCache; 28import android.support.v7.appcompat.R; 29import android.util.Log; 30import android.util.TypedValue; 31 32/** 33 * @hide 34 */ 35public class TintManager { 36 37 private static final String TAG = TintManager.class.getSimpleName(); 38 private static final boolean DEBUG = false; 39 private static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN; 40 41 private static final ColorFilterLruCache COLOR_FILTER_CACHE = new ColorFilterLruCache(6); 42 43 /** 44 * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal}, 45 * using the default mode. 46 */ 47 private static final int[] TINT_COLOR_CONTROL_NORMAL = { 48 R.drawable.abc_ic_ab_back_mtrl_am_alpha, 49 R.drawable.abc_ic_go_search_api_mtrl_alpha, 50 R.drawable.abc_ic_search_api_mtrl_alpha, 51 R.drawable.abc_ic_commit_search_api_mtrl_alpha, 52 R.drawable.abc_ic_clear_mtrl_alpha, 53 R.drawable.abc_ic_menu_share_mtrl_alpha, 54 R.drawable.abc_ic_menu_copy_mtrl_am_alpha, 55 R.drawable.abc_ic_menu_cut_mtrl_alpha, 56 R.drawable.abc_ic_menu_selectall_mtrl_alpha, 57 R.drawable.abc_ic_menu_paste_mtrl_am_alpha, 58 R.drawable.abc_ic_menu_moreoverflow_mtrl_alpha, 59 R.drawable.abc_ic_voice_search_api_mtrl_alpha, 60 R.drawable.abc_textfield_search_default_mtrl_alpha, 61 R.drawable.abc_textfield_default_mtrl_alpha, 62 R.drawable.abc_list_divider_mtrl_alpha 63 }; 64 65 /** 66 * Drawables which should be tinted with the value of {@code R.attr.colorControlActivated}, 67 * using the default mode. 68 */ 69 private static final int[] TINT_COLOR_CONTROL_ACTIVATED = { 70 R.drawable.abc_textfield_activated_mtrl_alpha, 71 R.drawable.abc_textfield_search_activated_mtrl_alpha, 72 R.drawable.abc_cab_background_top_mtrl_alpha 73 }; 74 75 /** 76 * Drawables which should be tinted with the value of {@code android.R.attr.colorBackground}, 77 * using the {@link android.graphics.PorterDuff.Mode#MULTIPLY} mode. 78 */ 79 private static final int[] TINT_COLOR_BACKGROUND_MULTIPLY = { 80 R.drawable.abc_popup_background_mtrl_mult, 81 R.drawable.abc_cab_background_internal_bg 82 }; 83 84 /** 85 * Drawables which should be tinted using a state list containing values of 86 * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated} 87 */ 88 private static final int[] TINT_COLOR_CONTROL_STATE_LIST = { 89 R.drawable.abc_edit_text_material, 90 R.drawable.abc_tab_indicator_material, 91 R.drawable.abc_textfield_search_material, 92 R.drawable.abc_spinner_mtrl_am_alpha 93 }; 94 95 /** 96 * Drawables which contain other drawables which should be tinted. The child drawable IDs 97 * should be defined in one of the arrays above. 98 */ 99 private static final int[] CONTAINERS_WITH_TINT_CHILDREN = { 100 R.drawable.abc_cab_background_top_material 101 }; 102 103 private final Context mContext; 104 private final Resources mResources; 105 private final TypedValue mTypedValue; 106 107 private ColorStateList mDefaultColorStateList; 108 109 /** 110 * A helper method to instantiate a {@link TintManager} and then call {@link #getDrawable(int)}. 111 * This method should not be used routinely. 112 */ 113 public static Drawable getDrawable(Context context, int resId) { 114 if (isInTintList(resId)) { 115 return new TintManager(context).getDrawable(resId); 116 } else { 117 return ContextCompat.getDrawable(context, resId); 118 } 119 } 120 121 public TintManager(Context context) { 122 mContext = context; 123 mResources = new TintResources(context.getResources(), this); 124 mTypedValue = new TypedValue(); 125 } 126 127 public Drawable getDrawable(int resId) { 128 Drawable drawable = ContextCompat.getDrawable(mContext, resId); 129 130 if (drawable != null) { 131 if (arrayContains(TINT_COLOR_CONTROL_STATE_LIST, resId)) { 132 drawable = new TintDrawableWrapper(drawable, getDefaultColorStateList()); 133 } else if (arrayContains(CONTAINERS_WITH_TINT_CHILDREN, resId)) { 134 drawable = mResources.getDrawable(resId); 135 } else { 136 tintDrawable(resId, drawable); 137 } 138 } 139 return drawable; 140 } 141 142 void tintDrawable(int resId, Drawable drawable) { 143 PorterDuff.Mode tintMode = null; 144 boolean colorAttrSet = false; 145 int colorAttr = 0; 146 147 if (arrayContains(TINT_COLOR_CONTROL_NORMAL, resId)) { 148 colorAttr = R.attr.colorControlNormal; 149 colorAttrSet = true; 150 } else if (arrayContains(TINT_COLOR_CONTROL_ACTIVATED, resId)) { 151 colorAttr = R.attr.colorControlActivated; 152 colorAttrSet = true; 153 } else if (arrayContains(TINT_COLOR_BACKGROUND_MULTIPLY, resId)) { 154 colorAttr = android.R.attr.colorBackground; 155 colorAttrSet = true; 156 tintMode = PorterDuff.Mode.MULTIPLY; 157 } 158 159 if (colorAttrSet) { 160 if (tintMode == null) { 161 tintMode = DEFAULT_MODE; 162 } 163 final int color = getThemeAttrColor(colorAttr); 164 165 // First, lets see if the cache already contains the color filter 166 PorterDuffColorFilter filter = COLOR_FILTER_CACHE.get(color, tintMode); 167 168 if (filter == null) { 169 // Cache miss, so create a color filter and add it to the cache 170 filter = new PorterDuffColorFilter(color, tintMode); 171 COLOR_FILTER_CACHE.put(color, tintMode, filter); 172 } 173 174 // Finally set the color filter 175 drawable.setColorFilter(filter); 176 177 if (DEBUG) { 178 Log.d(TAG, "Tinted Drawable ID: " + mResources.getResourceName(resId) + 179 " with color: #" + Integer.toHexString(color)); 180 } 181 } 182 } 183 184 private static boolean arrayContains(int[] array, int value) { 185 for (int id : array) { 186 if (id == value) { 187 return true; 188 } 189 } 190 return false; 191 } 192 193 private static boolean isInTintList(int drawableId) { 194 return arrayContains(TINT_COLOR_BACKGROUND_MULTIPLY, drawableId) || 195 arrayContains(TINT_COLOR_CONTROL_NORMAL, drawableId) || 196 arrayContains(TINT_COLOR_CONTROL_ACTIVATED, drawableId) || 197 arrayContains(TINT_COLOR_CONTROL_STATE_LIST, drawableId) || 198 arrayContains(CONTAINERS_WITH_TINT_CHILDREN, drawableId); 199 } 200 201 private ColorStateList getDefaultColorStateList() { 202 if (mDefaultColorStateList == null) { 203 /** 204 * Generate the default color state list which uses the colorControl attributes. 205 * Order is important here. The default enabled state needs to go at the bottom. 206 */ 207 208 final int colorControlNormal = getThemeAttrColor(R.attr.colorControlNormal); 209 final int colorControlActivated = getThemeAttrColor(R.attr.colorControlActivated); 210 211 final int[][] states = new int[7][]; 212 final int[] colors = new int[7]; 213 int i = 0; 214 215 // Disabled state 216 states[i] = new int[] { -android.R.attr.state_enabled }; 217 colors[i] = getDisabledThemeAttrColor(R.attr.colorControlNormal); 218 i++; 219 220 states[i] = new int[] { android.R.attr.state_focused }; 221 colors[i] = colorControlActivated; 222 i++; 223 224 states[i] = new int[] { android.R.attr.state_activated }; 225 colors[i] = colorControlActivated; 226 i++; 227 228 states[i] = new int[] { android.R.attr.state_pressed }; 229 colors[i] = colorControlActivated; 230 i++; 231 232 states[i] = new int[] { android.R.attr.state_checked }; 233 colors[i] = colorControlActivated; 234 i++; 235 236 states[i] = new int[] { android.R.attr.state_selected }; 237 colors[i] = colorControlActivated; 238 i++; 239 240 // Default enabled state 241 states[i] = new int[0]; 242 colors[i] = colorControlNormal; 243 i++; 244 245 mDefaultColorStateList = new ColorStateList(states, colors); 246 } 247 return mDefaultColorStateList; 248 } 249 250 int getThemeAttrColor(int attr) { 251 if (mContext.getTheme().resolveAttribute(attr, mTypedValue, true)) { 252 if (mTypedValue.type >= TypedValue.TYPE_FIRST_INT 253 && mTypedValue.type <= TypedValue.TYPE_LAST_INT) { 254 return mTypedValue.data; 255 } else if (mTypedValue.type == TypedValue.TYPE_STRING) { 256 return mResources.getColor(mTypedValue.resourceId); 257 } 258 } 259 return 0; 260 } 261 262 int getDisabledThemeAttrColor(int attr) { 263 final int color = getThemeAttrColor(attr); 264 final int originalAlpha = Color.alpha(color); 265 266 // Now retrieve the disabledAlpha value from the theme 267 mContext.getTheme().resolveAttribute(android.R.attr.disabledAlpha, mTypedValue, true); 268 final float disabledAlpha = mTypedValue.getFloat(); 269 270 // Return the color, multiplying the original alpha by the disabled value 271 return (color & 0x00ffffff) | (Math.round(originalAlpha * disabledAlpha) << 24); 272 } 273 274 private static class ColorFilterLruCache extends LruCache<Integer, PorterDuffColorFilter> { 275 276 public ColorFilterLruCache(int maxSize) { 277 super(maxSize); 278 } 279 280 PorterDuffColorFilter get(int color, PorterDuff.Mode mode) { 281 return get(generateCacheKey(color, mode)); 282 } 283 284 PorterDuffColorFilter put(int color, PorterDuff.Mode mode, PorterDuffColorFilter filter) { 285 return put(generateCacheKey(color, mode), filter); 286 } 287 288 private static int generateCacheKey(int color, PorterDuff.Mode mode) { 289 int hashCode = 1; 290 hashCode = 31 * hashCode + color; 291 hashCode = 31 * hashCode + mode.hashCode(); 292 return hashCode; 293 } 294 } 295} 296