Home | History | Annotate | Download | only in res
      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