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