1/* 2 * Copyright (C) 2016 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.content.res; 18 19import android.content.Context; 20import android.content.res.ColorStateList; 21import android.content.res.Configuration; 22import android.content.res.Resources; 23import android.graphics.drawable.Drawable; 24import android.os.Build; 25import android.support.annotation.ColorRes; 26import android.support.annotation.DrawableRes; 27import android.support.annotation.NonNull; 28import android.support.annotation.Nullable; 29import android.support.v4.content.ContextCompat; 30import android.support.v7.widget.AppCompatDrawableManager; 31import android.util.Log; 32import android.util.SparseArray; 33import android.util.TypedValue; 34 35import org.xmlpull.v1.XmlPullParser; 36 37import java.util.WeakHashMap; 38 39/** 40 * Class for accessing an application's resources through AppCompat, and thus any backward 41 * compatible functionality. 42 */ 43public final class AppCompatResources { 44 45 private static final String LOG_TAG = "AppCompatResources"; 46 private static final ThreadLocal<TypedValue> TL_TYPED_VALUE = new ThreadLocal<>(); 47 48 private static final WeakHashMap<Context, SparseArray<ColorStateListCacheEntry>> 49 sColorStateCaches = new WeakHashMap<>(0); 50 51 private static final Object sColorStateCacheLock = new Object(); 52 53 private AppCompatResources() {} 54 55 /** 56 * Returns the {@link ColorStateList} from the given resource. The resource can include 57 * themeable attributes, regardless of API level. 58 * 59 * @param context context to inflate against 60 * @param resId the resource identifier of the ColorStateList to retrieve 61 */ 62 public static ColorStateList getColorStateList(@NonNull Context context, @ColorRes int resId) { 63 if (Build.VERSION.SDK_INT >= 23) { 64 // On M+ we can use the framework 65 return context.getColorStateList(resId); 66 } 67 68 // Before that, we'll try handle it ourselves 69 ColorStateList csl = getCachedColorStateList(context, resId); 70 if (csl != null) { 71 return csl; 72 } 73 // Cache miss, so try and inflate it ourselves 74 csl = inflateColorStateList(context, resId); 75 if (csl != null) { 76 // If we inflated it, add it to the cache and return 77 addColorStateListToCache(context, resId, csl); 78 return csl; 79 } 80 81 // If we reach here then we couldn't inflate it, so let the framework handle it 82 return ContextCompat.getColorStateList(context, resId); 83 } 84 85 /** 86 * Return a drawable object associated with a particular resource ID. 87 * 88 * <p>This method supports inflation of {@code <vector>} and {@code <animated-vector>} 89 * resources on devices where platform support is not available.</p> 90 * 91 * @param context context to inflate against 92 * @param resId The desired resource identifier, as generated by the aapt 93 * tool. This integer encodes the package, type, and resource 94 * entry. The value 0 is an invalid identifier. 95 * @return Drawable An object that can be used to draw this resource. 96 * @see ContextCompat#getDrawable(Context, int) 97 */ 98 @Nullable 99 public static Drawable getDrawable(@NonNull Context context, @DrawableRes int resId) { 100 return AppCompatDrawableManager.get().getDrawable(context, resId); 101 } 102 103 /** 104 * Inflates a {@link ColorStateList} from resources, honouring theme attributes. 105 */ 106 @Nullable 107 private static ColorStateList inflateColorStateList(Context context, int resId) { 108 if (isColorInt(context, resId)) { 109 // The resource is a color int, we can't handle it so return null 110 return null; 111 } 112 113 final Resources r = context.getResources(); 114 final XmlPullParser xml = r.getXml(resId); 115 try { 116 return AppCompatColorStateListInflater.createFromXml(r, xml, context.getTheme()); 117 } catch (Exception e) { 118 Log.e(LOG_TAG, "Failed to inflate ColorStateList, leaving it to the framework", e); 119 } 120 return null; 121 } 122 123 @Nullable 124 private static ColorStateList getCachedColorStateList(@NonNull Context context, 125 @ColorRes int resId) { 126 synchronized (sColorStateCacheLock) { 127 final SparseArray<ColorStateListCacheEntry> entries = sColorStateCaches.get(context); 128 if (entries != null && entries.size() > 0) { 129 final ColorStateListCacheEntry entry = entries.get(resId); 130 if (entry != null) { 131 if (entry.configuration.equals(context.getResources().getConfiguration())) { 132 // If the current configuration matches the entry's, we can use it 133 return entry.value; 134 } else { 135 // Otherwise we'll remove the entry 136 entries.remove(resId); 137 } 138 } 139 } 140 } 141 return null; 142 } 143 144 private static void addColorStateListToCache(@NonNull Context context, @ColorRes int resId, 145 @NonNull ColorStateList value) { 146 synchronized (sColorStateCacheLock) { 147 SparseArray<ColorStateListCacheEntry> entries = sColorStateCaches.get(context); 148 if (entries == null) { 149 entries = new SparseArray<>(); 150 sColorStateCaches.put(context, entries); 151 } 152 entries.append(resId, new ColorStateListCacheEntry(value, 153 context.getResources().getConfiguration())); 154 } 155 } 156 157 private static boolean isColorInt(@NonNull Context context, @ColorRes int resId) { 158 final Resources r = context.getResources(); 159 160 final TypedValue value = getTypedValue(); 161 r.getValue(resId, value, true); 162 163 return value.type >= TypedValue.TYPE_FIRST_COLOR_INT 164 && value.type <= TypedValue.TYPE_LAST_COLOR_INT; 165 } 166 167 @NonNull 168 private static TypedValue getTypedValue() { 169 TypedValue tv = TL_TYPED_VALUE.get(); 170 if (tv == null) { 171 tv = new TypedValue(); 172 TL_TYPED_VALUE.set(tv); 173 } 174 return tv; 175 } 176 177 private static class ColorStateListCacheEntry { 178 final ColorStateList value; 179 final Configuration configuration; 180 181 ColorStateListCacheEntry(@NonNull ColorStateList value, 182 @NonNull Configuration configuration) { 183 this.value = value; 184 this.configuration = configuration; 185 } 186 } 187 188} 189