/*
* Copyright (C) 2016 Bilibili
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.bilibili.magicasakura.utils;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.ColorRes;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.LruCache;
import android.support.v7.view.ContextThemeWrapper;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;
import com.bilibili.magicasakura.drawables.FilterableStateListDrawable;
import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.WeakHashMap;
/**
* @author xyczero617@gmail.com
* @time 15/9/15
*/
public class TintManager {
private static final String TAG = "TintManager";
private static final boolean DEBUG = false;
private static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN;
private static final String SKIP_DRAWABLE_TAG = "appcompat_skip_skip";
private static final WeakHashMap<Context, TintManager> INSTANCE_CACHE = new WeakHashMap<>();
private static final ColorFilterLruCache COLOR_FILTER_CACHE = new ColorFilterLruCache(6);
private final Object mDrawableCacheLock = new Object();
private WeakReference<Context> mContextRef;
private SparseArray<ColorStateList> mCacheTintList;
private SparseArray<WeakReference<Drawable.ConstantState>> mCacheDrawables;
private SparseArray<String> mSkipDrawableIdTags;
public static TintManager get(Context context) {
if (context == null) return null;
if (context instanceof ContextThemeWrapper) {
context = ((ContextThemeWrapper) context).getBaseContext();
}
if (context instanceof android.view.ContextThemeWrapper) {
context = ((android.view.ContextThemeWrapper) context).getBaseContext();
}
TintManager tm = INSTANCE_CACHE.get(context);
if (tm == null) {
tm = new TintManager(context);
INSTANCE_CACHE.put(context, tm);
printLog("[get TintManager] create new TintManager.");
}
return tm;
}
private TintManager(Context context) {
mContextRef = new WeakReference<>(context);
}
public static void clearTintCache() {
for (Map.Entry<Context, TintManager> entry : INSTANCE_CACHE.entrySet()) {
TintManager tm = entry.getValue();
if (tm != null)
tm.clear();
}
COLOR_FILTER_CACHE.evictAll();
}
private void clear() {
if (mCacheTintList != null) {
mCacheTintList.clear();
}
if (mCacheDrawables != null) {
mCacheDrawables.clear();
}
if (mSkipDrawableIdTags != null) {
mSkipDrawableIdTags.clear();
}
}
@Nullable
public ColorStateList getColorStateList(@ColorRes int resId) {
if (resId == 0) return null;
final Context context = mContextRef.get();
if (context == null) return null;
ColorStateList colorStateList = mCacheTintList != null ? mCacheTintList.get(resId) : null;
if (colorStateList == null) {
colorStateList = ColorStateListUtils.createColorStateList(context, resId);
if (colorStateList != null) {
if (mCacheTintList == null) {
mCacheTintList = new SparseArray<>();
}
mCacheTintList.append(resId, colorStateList);
}
}
return colorStateList;
}
@Nullable
public Drawable getDrawable(@DrawableRes int resId) {
final Context context = mContextRef.get();
if (context == null) return null;
if (resId == 0) return null;
if (mSkipDrawableIdTags != null) {
final String cachedTagName = mSkipDrawableIdTags.get(resId);
if (SKIP_DRAWABLE_TAG.equals(cachedTagName)) {
printLog("[Match Skip DrawableTag] Skip the drawable which is matched with the skip tag.");
return null;
}
} else {
// Create an id cache as we'll need one later
mSkipDrawableIdTags = new SparseArray<>();
}
// Try the cache first (if it exists)
Drawable drawable = getCacheDrawable(context, resId);
if (drawable == null) {
drawable = DrawableUtils.createDrawable(context, resId);
if (drawable != null && !(drawable instanceof ColorDrawable)) {
if (addCachedDrawable(resId, drawable)) {
printLog("[loadDrawable] Saved drawable to cache: " +
context.getResources().getResourceName(resId));
}
}
}
if (drawable == null) {
mSkipDrawableIdTags.append(resId, SKIP_DRAWABLE_TAG);
}
return drawable;
}
private Drawable getCacheDrawable(@NonNull final Context context, final int key) {
synchronized (mDrawableCacheLock) {
if (mCacheDrawables == null) return null;
final WeakReference<Drawable.ConstantState> weakReference = mCacheDrawables.get(key);
if (weakReference != null) {
Drawable.ConstantState cs = weakReference.get();
if (cs != null) {
printLog("[getCacheDrawable] Get drawable from cache: " +
context.getResources().getResourceName(key));
return cs.newDrawable();
} else {
mCacheDrawables.delete(key);
}
}
}
return null;
}
private boolean addCachedDrawable(final int key, @NonNull final Drawable drawable) {
if (drawable instanceof FilterableStateListDrawable) {
return false;
}
final Drawable.ConstantState cs = drawable.getConstantState();
if (cs != null) {
synchronized (mDrawableCacheLock) {
if (mCacheDrawables == null) {
mCacheDrawables = new SparseArray<>();
}
mCacheDrawables.put(key, new WeakReference<>(cs));
}
return true;
}
return false;
}
private static class ColorFilterLruCache extends LruCache<Integer, PorterDuffColorFilter> {
public ColorFilterLruCache(int maxSize) {
super(maxSize);
}
PorterDuffColorFilter get(int color, PorterDuff.Mode mode) {
return get(generateCacheKey(color, mode));
}
PorterDuffColorFilter put(int color, PorterDuff.Mode mode, PorterDuffColorFilter filter) {
return put(generateCacheKey(color, mode), filter);
}
private static int generateCacheKey(int color, PorterDuff.Mode mode) {
int hashCode = 1;
hashCode = 31 * hashCode + color;
hashCode = 31 * hashCode + mode.hashCode();
return hashCode;
}
}
public static void tintViewBackground(View view, TintInfo tint) {
Drawable background;
if (view == null || (background = view.getBackground()) == null) return;
if (tint.mHasTintList || tint.mHasTintMode) {
background.mutate();
if (background instanceof ColorDrawable) {
((ColorDrawable) background).setColor(ThemeUtils.replaceColor(view.getContext(), tint.mTintList.getColorForState(view.getDrawableState(), tint.mTintList.getDefaultColor())));
} else {
background.setColorFilter(createTintFilter(view.getContext(),
tint.mHasTintList ? tint.mTintList : null,
tint.mHasTintMode ? tint.mTintMode : DEFAULT_MODE,
view.getDrawableState()));
}
} else {
background.clearColorFilter();
}
if (Build.VERSION.SDK_INT <= 23) {
// On Gingerbread, GradientDrawable does not invalidate itself when it's ColorFilter
// has changed, so we need to force an invalidation
background.invalidateSelf();
}
}
public static void tintViewDrawable(View view, Drawable drawable, TintInfo tint) {
if (view == null || drawable == null) return;
if (tint.mHasTintList || tint.mHasTintMode) {
drawable.mutate();
if (drawable instanceof ColorDrawable) {
((ColorDrawable) drawable).setColor(ThemeUtils.replaceColor(view.getContext(), tint.mTintList.getColorForState(view.getDrawableState(), tint.mTintList.getDefaultColor())));
} else {
drawable.setColorFilter(createTintFilter(view.getContext(),
tint.mHasTintList ? tint.mTintList : null,
tint.mHasTintMode ? tint.mTintMode : DEFAULT_MODE,
view.getDrawableState()));
}
} else {
drawable.clearColorFilter();
}
if (Build.VERSION.SDK_INT <= 23) {
// On Gingerbread, GradientDrawable does not invalidate itself when it's ColorFilter
// has changed, so we need to force an invalidation
drawable.invalidateSelf();
}
}
private static PorterDuffColorFilter createTintFilter(Context context, ColorStateList tint, PorterDuff.Mode tintMode, final int[] state) {
if (tint == null || tintMode == null) {
return null;
}
final int color = ThemeUtils.replaceColor(context, tint.getColorForState(state, tint.getDefaultColor()));
return getPorterDuffColorFilter(color, tintMode);
}
private static PorterDuffColorFilter getPorterDuffColorFilter(int color, PorterDuff.Mode mode) {
// First, lets see if the cache already contains the color filter
PorterDuffColorFilter filter = COLOR_FILTER_CACHE.get(color, mode);
if (filter == null) {
// Cache miss, so create a color filter and add it to the cache
filter = new PorterDuffColorFilter(color, mode);
COLOR_FILTER_CACHE.put(color, mode, filter);
}
return filter;
}
private static void printLog(String msg) {
if (DEBUG) {
Log.i(TAG, msg);
}
}
}