首先所有皮肤的view——skinView:如ImageView
public class SkinView {
private View mSkView;//ImageView
private List<SkinAttr> mSkinAttrs;//src,backgroud
public SkinView(View mSkView, List<SkinAttr> mSkinAttrs) {
this.mSkView = mSkView;
this.mSkinAttrs = mSkinAttrs;
}
public void skin(){
for (SkinAttr skinAttr : mSkinAttrs) {
skinAttr.skin(mSkView);
}
}
}
所有view的属性——SkinAttr
public class SkinAttr {
private String mResName;//如资源的名字,meinv.jpg
private SkinType mSkinType;//textColor,background
public SkinAttr(String mResName, SkinType mSkinType) {
this.mResName = mResName;
this.mSkinType = mSkinType;
}
public void skin(View view) {
mSkinType.skin(view,mResName);
}
}
每个类型进行判断并设置
public enum SkinType {
TEXT_COLO("textColor") {
@Override
public void skin(View view, String resName) {
SkinResource resource = getSkinResource();
ColorStateList color = resource.getColorByName(resName);
if (color != null) {
TextView tv = (TextView) view;
tv.setTextColor(color);
}
}
}, BACKGROUND("background") {
@Override
public void skin(View view, String resName) {
SkinResource resource = getSkinResource();
Drawable drawable = resource.getDrawableByName(resName);
ImageView imageView = (ImageView) view;
if (drawable != null) {
imageView.setBackgroundDrawable(drawable);
return;
}
//有可能是颜色
ColorStateList color = resource.getColorByName(resName);
if (color != null) {
view.setBackgroundColor(color.getDefaultColor());
}
}
}, SRC("src") {
@Override
public void skin(View view, String resName) {
SkinResource resource = getSkinResource();
Drawable drawable = resource.getDrawableByName(resName);
if (drawable != null) {
ImageView imageView = (ImageView) view;
imageView.setImageDrawable(drawable);
return;
}
}
};
private String mResName;
SkinType(String resName) {
this.mResName = resName;
}
public abstract void skin(View view, String resName);
/**
* 获得资源的名字(meinv.jpg)
*/
public String getResName() {
return mResName;
}
/**
* 获得皮肤
*/
public SkinResource getSkinResource() {
return SkinManager.getInstance().getSkinResource();
}
}
皮肤属性解析辅助类SkinAttrSupport
public class SkinAttrSupport {
private static final String TAG = SkinAttrSupport.class.getSimpleName();
public static List<SkinAttr> skin(Context context, AttributeSet attrs) {
List<SkinAttr> skinAttrs = new ArrayList<>();
int count = attrs.getAttributeCount();
for (int index = 0; index < count; index++) {//layout_marginTop,----->10.0dip
//Log.e(TAG, attrs.getAttributeName(index)+",----->"+attrs.getAttributeValue(index));
//获得属性的名字和值
String attrName = attrs.getAttributeName(index);
String attrValue = attrs.getAttributeValue(index);
SkinType skinType = getSkinType(attrName);
if (skinType != null) {
String resName = getResName(context, attrValue);
//Log.e(TAG,resName);//image_src
if(TextUtils.isEmpty(resName)){
continue;
}
SkinAttr skinAttr = new SkinAttr(resName, skinType);
skinAttrs.add(skinAttr);
}
}
return skinAttrs;
}
/**
* 获得资源的名字
*/
private static String getResName(Context context, String attrs) {//textSize=15
if (attrs.startsWith("@")) {
attrs = attrs.substring(1);//从1开始截取
int resId = Integer.parseInt(attrs);
return context.getResources().getResourceEntryName(resId);//设置的图片的资源的名字
}
return null;
}
/**
* 获得属性的名字
*/
private static SkinType getSkinType(String attrName) {
SkinType[] skinTypes = SkinType.values();
for (SkinType skinType : skinTypes) {
if (skinType.getResName().equals(attrName)) {
return skinType;
}
}
return null;
}
}
获取资源——skinResource
public class SkinResource {
private Resources mSkinResource;
private String mPackName;
private String TAG=SkinResource.class.getSimpleName();
public SkinResource(Context context, String skinPath) {
Resources superResource = context.getResources();
try {
AssetManager assets = AssetManager.class.newInstance();//{@hide}
Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
method.invoke(assets, skinPath);
mSkinResource = new Resources
(assets, superResource.getDisplayMetrics(), superResource.getConfiguration());
//获取包名
mPackName = context.getPackageManager()
.getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES).packageName;
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取资源
*/
public Drawable getDrawableByName(String resName) {
try {
int resourceId = mSkinResource.getIdentifier(resName, "drawable", mPackName);
//Log.e(TAG,resName+"——>mPackName"+mPackName);
Drawable drawable = mSkinResource.getDrawable(resourceId);
return drawable;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取颜色
*/
public ColorStateList getColorByName(String resName) {
try {
int resourceId = mSkinResource.getIdentifier(resName, "drawable", mPackName);
ColorStateList color = mSkinResource.getColorStateList(resourceId);
return color;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
皮肤管理
public class SkinManager {
private static final SkinManager mInstance;
private Context mContext;
private Map<Activity, List<SkinView>> mSkinView = new HashMap<>();
private SkinResource mSkinResource;
public void init(Context context) {
mContext = context.getApplicationContext();
String skinPath = SkinPreUtils.getInstance(context).getSkinPath();
File file = new File(skinPath);
if (!file.exists()) {
SkinPreUtils.getInstance(context).clearSkinInfo();
return;
}
//包名不存在
String packageName = context.getPackageManager().getPackageArchiveInfo
(skinPath, PackageManager.GET_ACTIVITIES).packageName;
if (TextUtils.isEmpty(packageName)) {
SkinPreUtils.getInstance(mContext).clearSkinInfo();
return;
}
mSkinResource = new SkinResource(mContext, skinPath);
}
static {
mInstance = new SkinManager();
}
public static SkinManager getInstance() {
return mInstance;
}
//换肤
public int loadSkin(String skinPath) {
File file = new File(skinPath);
if (!file.exists()) {
return SkinConfig.SKIN_FILE_NOEXSIST;//文件不存在
}
//包名不存在
String packageName = mContext.getPackageManager().getPackageArchiveInfo
(skinPath, PackageManager.GET_ACTIVITIES).packageName;
if (TextUtils.isEmpty(packageName)) {
return SkinConfig.SKIN_FILE_ERROR;//不是一个apk
}
//如果当前是一样的就不换肤
String currentPath = SkinPreUtils.getInstance(mContext).getSkinPath();
if (currentPath.equals(skinPath)) {
return SkinConfig.SKIN_CHANGE_NOTHING;
}
mSkinResource = new SkinResource(mContext, skinPath);
changeSkin();
//4.保存皮肤
SkinPreUtils.getInstance(mContext).saveSkinPath(skinPath);
return 0;
}
/**
* 切换皮肤
*/
private void changeSkin() {
Set<Activity> keys = mSkinView.keySet();
for (Activity key : keys) {
List<SkinView> skinViews = mSkinView.get(key);
for (SkinView skinView : skinViews) {
skinView.skin();
}
}
}
//恢复默认
public int restoreDefault() {
String currentSkinPath = SkinPreUtils.getInstance(mContext).getSkinPath();
if (TextUtils.isEmpty(currentSkinPath)) {
return SkinConfig.SKIN_CHANGE_NOTHING;
}
//当前手机运行的路径
String skinPath = mContext.getPackageResourcePath();
mSkinResource = new SkinResource(mContext, skinPath);
//改变皮肤
changeSkin();
//清空皮肤信息
SkinPreUtils.getInstance(mContext).clearSkinInfo();
return SkinConfig.SKIN_CHANGE_SUCCESS;
}
public List<SkinView> getSkinView(Activity activity) {
return mSkinView.get(activity);
}
public SkinResource getSkinResource() {
return mSkinResource;
}
public void register(Activity activity, List<SkinView> skinViews) {
mSkinView.put(activity, skinViews);
}
/**
* 检查是否需要切换
*
* @param skinView
*/
public void checkChangeSkin(SkinView skinView) {
String skinPath = SkinPreUtils.getInstance(mContext).getSkinPath();
if (!TextUtils.isEmpty(skinPath)) {
skinView.skin();
}
}
public void unRegister(Activity activity) {
mSkinView.remove(activity);
}
}
拦截view
public abstract class BaseSkinActivity extends BaseActivity implements LayoutInflaterFactory {
private SkinAppCompatViewInflater mAppCompatViewInflater;
private String TAG = "BaseSkinActivity";
private static final boolean IS_PRE_LOLLIPOP = Build.VERSION.SDK_INT < 21;
//只能放一些通用的方法,基本每个activity都需要使用的方法
// 如果是两个或两个以上的地方要使用,最好写一个工具类
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
//拦截到View的创建 获取View之后去解析
LayoutInflater layoutInflater = LayoutInflater.from(this);
LayoutInflaterCompat.setFactory(layoutInflater, this);//会调用onCreateView方法
super.onCreate(savedInstanceState);
}
public View onCreateView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
// //1.创建View
final View view = createView(parent, name, context, attrs);
// Log.e(TAG, view + "");
if (view != null) {
// 2. 解析属性 src textColor background 自定义属性
// 2.1 一个activity的布局肯定对应多个这样的 SkinView
List<SkinAttr> skinAttrs = SkinAttrSupport.skin(context, attrs);
SkinView skinView = new SkinView(view, skinAttrs);
// 3.统一交给SkinManager管理
managerSkinView(skinView);
//4.判断是否需要换肤
SkinManager.getInstance().checkChangeSkin(skinView);
return view;
}
return view;
}
protected void managerSkinView(SkinView skinView) {
List<SkinView> skinViews = SkinManager.getInstance().getSkinView(this);
if (skinViews == null) {
skinViews = new ArrayList<>();
SkinManager.getInstance().register(this, skinViews);
}
skinViews.add(skinView);
}
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
if (mAppCompatViewInflater == null) {
mAppCompatViewInflater = new SkinAppCompatViewInflater();
}
boolean inheritContext = false;
if (IS_PRE_LOLLIPOP) {
inheritContext = (attrs instanceof XmlPullParser)
// If we have a XmlPullParser, we can detect where we are in the layout
? ((XmlPullParser) attrs).getDepth() > 1
// Otherwise we have to use the old heuristic
: shouldInheritContext((ViewParent) parent);
}
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
true, /* Read read app:theme as a fallback at all times for legacy reasons */
VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
);
}
private boolean shouldInheritContext(ViewParent parent) {
if (parent == null) {
// The initial parent is null so just return false
return false;
}
final View windowDecor = getWindow().getDecorView();
while (true) {
if (parent == null) {
// Bingo. We've hit a view which has a null parent before being terminated from
// the loop. This is (most probably) because it's the root view in an inflation
// call, therefore we should inherit. This works as the inflated layout is only
// added to the hierarchy at the end of the inflate() call.
return true;
} else if (parent == windowDecor || !(parent instanceof View)
|| ViewCompat.isAttachedToWindow((View) parent)) {
// We have either hit the window's decor view, a parent which isn't a View
// (i.e. ViewRootImpl), or an attached view, so we know that the original parent
// is currently added to the view hierarchy. This means that it has not be
// inflated in the current inflate() call and we should not inherit the context.
return false;
}
parent = parent.getParent();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
SkinManager.getInstance().unRegister(this);
}
}
工具类封装
资源文件配置SkinConfig
public class SkinConfig {
// SP的文件名称
public static final String SKIN_INFO_NAME = "skinInfo";
// 保存皮肤文件的路径的名称
public static final String SKIN_PATH_NAME = "skinPath";
// 不需要改变任何东西
public static final int SKIN_CHANGE_NOTHING = -1;
// 换肤成功
public static final int SKIN_CHANGE_SUCCESS = 1;
// 皮肤文件不存在
public static final int SKIN_FILE_NOEXSIST = -2;
// 皮肤文件有错误可能不是一个apk文件
public static final int SKIN_FILE_ERROR = -3;
}
sp保存
public class SkinPreUtils {
private static SkinPreUtils mInstance;
private static Context mContext;
private SkinPreUtils(Context context) {
this.mContext = context.getApplicationContext();
}
public static SkinPreUtils getInstance(Context context) {
if (mInstance == null) {
synchronized (SkinPreUtils.class) {
if (mInstance == null) {
mInstance = new SkinPreUtils(context);
}
}
}
return mInstance;
}
/**
* 保存当前皮肤路径
*
* @param skinPath
*/
public void saveSkinPath(String skinPath) {
mContext.getSharedPreferences(SkinConfig.SKIN_INFO_NAME, Context.MODE_PRIVATE)
.edit().putString(SkinConfig.SKIN_PATH_NAME, skinPath).commit();
}
/**
* 获取皮肤的路径
*
* @return 当前皮肤路径
*/
public String getSkinPath() {
return mContext.getSharedPreferences(SkinConfig.SKIN_INFO_NAME, Context.MODE_PRIVATE)
.getString(SkinConfig.SKIN_PATH_NAME, "");
}
/**
* 清空皮肤路径
*/
public void clearSkinInfo() {
saveSkinPath("");
}
}
SkinAppCompatViewInflate:是Google官方提供的源码,因为源码不是public所以不能直接使用
public class SkinAppCompatViewInflater {
private static final Class<?>[] sConstructorSignature = new Class[]{
Context.class, AttributeSet.class};
private static final int[] sOnClickAttrs = new int[]{android.R.attr.onClick};
private static final String[] sClassPrefixList = {
"android.widget.",
"android.view.",
"android.webkit."
};
private static final String LOG_TAG = "AppCompatViewInflater";
private static final Map<String, Constructor<? extends View>> sConstructorMap
= new ArrayMap<>();
private final Object[] mConstructorArgs = new Object[2];
public final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
final Context originalContext = context;
// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
// by using the parent's context
if (inheritContext && parent != null) {
context = parent.getContext();
}
if (readAndroidTheme || readAppTheme) {
// We then apply the theme on the context, if specified
context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
}
if (wrapContext) {
context = TintContextWrapper.wrap(context);
}
View view = null;
// We need to 'inject' our tint aware Views in place of the standard framework versions
switch (name) {
case "TextView":
view = new AppCompatTextView(context, attrs);
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
case "Button":
view = new AppCompatButton(context, attrs);
break;
case "EditText":
view = new AppCompatEditText(context, attrs);
break;
case "Spinner":
view = new AppCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new AppCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new AppCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new AppCompatRadioButton(context, attrs);
break;
case "CheckedTextView":
view = new AppCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new AppCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new AppCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new AppCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new AppCompatSeekBar(context, attrs);
break;
}
if (view == null && originalContext != context) {
// If the original context does not equal our themed context, then we need to manually
// inflate it using the name so that android:theme takes effect.
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check it's android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
private View createViewFromTag(Context context, String name, AttributeSet attrs) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
try {
mConstructorArgs[0] = context;
mConstructorArgs[1] = attrs;
if (-1 == name.indexOf('.')) {
for (int i = 0; i < sClassPrefixList.length; i++) {
final View view = createView(context, name, sClassPrefixList[i]);
if (view != null) {
return view;
}
}
return null;
} else {
return createView(context, name, null);
}
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
} finally {
// Don't retain references on context.
mConstructorArgs[0] = null;
mConstructorArgs[1] = null;
}
}
/**
* android:onClick doesn't handle views with a ContextWrapper context. This method
* backports new framework functionality to traverse the Context wrappers to find a
* suitable target.
*/
private void checkOnClickListener(View view, AttributeSet attrs) {
final Context context = view.getContext();
if (!(context instanceof ContextWrapper) ||
(Build.VERSION.SDK_INT >= 15 && !ViewCompat.hasOnClickListeners(view))) {
// Skip our compat functionality if: the Context isn't a ContextWrapper, or
// the view doesn't have an OnClickListener (we can only rely on this on API 15+ so
// always use our compat code on older devices)
return;
}
final TypedArray a = context.obtainStyledAttributes(attrs, sOnClickAttrs);
final String handlerName = a.getString(0);
if (handlerName != null) {
view.setOnClickListener(new DeclaredOnClickListener(view, handlerName));
}
a.recycle();
}
private View createView(Context context, String name, String prefix)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
Class<? extends View> clazz = context.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
constructor = clazz.getConstructor(sConstructorSignature);
sConstructorMap.put(name, constructor);
}
constructor.setAccessible(true);
return constructor.newInstance(mConstructorArgs);
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
}
}
/**
* Allows us to emulate the {@code android:theme} attribute for devices before L.
*/
private static Context themifyContext(Context context, AttributeSet attrs,
boolean useAndroidTheme, boolean useAppTheme) {
final TypedArray a = context.obtainStyledAttributes(attrs, android.support.v7.appcompat.R.styleable.View, 0, 0);
int themeId = 0;
if (useAndroidTheme) {
// First try reading android:theme if enabled
themeId = a.getResourceId(android.support.v7.appcompat.R.styleable.View_android_theme, 0);
}
if (useAppTheme && themeId == 0) {
// ...if that didn't work, try reading app:theme (for legacy reasons) if enabled
themeId = a.getResourceId(android.support.v7.appcompat.R.styleable.View_theme, 0);
if (themeId != 0) {
Log.i(LOG_TAG, "app:theme is now deprecated. "
+ "Please move to using android:theme instead.");
}
}
a.recycle();
if (themeId != 0 && (!(context instanceof ContextThemeWrapper)
|| ((ContextThemeWrapper) context).getThemeResId() != themeId)) {
// If the context isn't a ContextThemeWrapper, or it is but does not have
// the same theme as we need, wrap it in a new wrapper
context = new ContextThemeWrapper(context, themeId);
}
return context;
}
/**
* An implementation of OnClickListener that attempts to lazily load a
* named click handling method from a parent or ancestor context.
*/
private static class DeclaredOnClickListener implements View.OnClickListener {
private final View mHostView;
private final String mMethodName;
private Method mResolvedMethod;
private Context mResolvedContext;
public DeclaredOnClickListener(@NonNull View hostView, @NonNull String methodName) {
mHostView = hostView;
mMethodName = methodName;
}
@Override
public void onClick(@NonNull View v) {
if (mResolvedMethod == null) {
resolveMethod(mHostView.getContext(), mMethodName);
}
try {
mResolvedMethod.invoke(mResolvedContext, v);
} catch (IllegalAccessException e) {
throw new IllegalStateException(
"Could not execute non-public method for android:onClick", e);
} catch (InvocationTargetException e) {
throw new IllegalStateException(
"Could not execute method for android:onClick", e);
}
}
@NonNull
private void resolveMethod(@Nullable Context context, @NonNull String name) {
while (context != null) {
try {
if (!context.isRestricted()) {
final Method method = context.getClass().getMethod(mMethodName, View.class);
if (method != null) {
mResolvedMethod = method;
mResolvedContext = context;
return;
}
}
} catch (NoSuchMethodException e) {
// Failed to find method, keep searching up the hierarchy.
}
if (context instanceof ContextWrapper) {
context = ((ContextWrapper) context).getBaseContext();
} else {
// Can't search up the hierarchy, null out and fail.
context = null;
}
}
final int id = mHostView.getId();
final String idText = id == View.NO_ID ? "" : " with id '"
+ mHostView.getContext().getResources().getResourceEntryName(id) + "'";
throw new IllegalStateException("Could not find method " + mMethodName
+ "(View) in a parent or ancestor Context for android:onClick "
+ "attribute defined on view " + mHostView.getClass() + idText);
}
}
}
使用
public void skin(View view) {
//换肤
String skinPath= Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator
+ "Download"+File.separator+"meinv.skin";
int loadSkin = SkinManager.getInstance().loadSkin(skinPath);
}
public void skin1(View view) {
//默认
SkinManager.getInstance().restoreDefault();
}
public void skin2(View view) {
//跳转
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
}