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