1. 概述
接着上篇文章来分析,目前我们能够做到的就是能够获取另一个本地没有安装的apk的资源文件,而按道理来讲,我们的apk是需要从网上去下载的,但是这里我们为了做演示,就直接把一个 demo打成apk包,然后命名为 red.skin文件手动复制到 手机存储目录中,如果不是很清楚这个换肤 代码实现的,可以先去看我之前的文章
2. 实现换肤后的效果
1>:每一次打开的都是新的皮肤;
2>:换肤之后所有的Activity里面的View都要换肤;
3>:每次重新进入app也需要换肤;
3. 解决方案
由上边换肤后的效果可知,我们在换肤后需要实现上边的3个效果,解决方案如下:
方案1>:把每一个 activity中需要换肤的 View都找出来,然后调用代码换肤,这个是最死板的方法;
方案2>:获取Activity里边的 根布局,然后遍历根布局,不断的循环获取子View,通过解析 tag,然后进行换肤;
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/test_btn"
android:text="换肤"
android:tag="skin:background:blue_color@@textColor:black_color"
/>
方案3>:拦截View的创建,这个目前是最好的方法,分析图如下
由上图可知:
第一步:比如说在一个Activity中有多个控件,这里暂且就定为有Button、TextView、ImageView等3个控件;
第二步:在右边定义一个集合,用于保存Activity中这3个控件的实例对象;第三步:只要点击换肤按钮,就去遍历集合中的所有对象,该修改颜色的修改颜色,该修改背景的修改背景,就ok;
那么既然我们需要使用第三种方案,拦截View的创建,那么我们就首先要知道这些Activity的页面是如何创建的,那么下篇文章我会带大家来分析setContentView的源码,我们一起来探索 Activity的页面到底是如何创建的。
4. 具体思路
自己在测试时,是直接打包了一个red.skin皮肤包,然后复制到手机存储目录,然后直接调用换肤的方法,然后把这个皮肤包的路径传递就ok,但是在实际开发中,我们是需要从服务器端去下载皮肤包,然后把直接把路径传递给 换肤的方法loadSkin(path)就ok。
注意
这里的 red.skin包是一个apk文件,然后以.skin结尾命名的,apk中没有任何代码,只是在drawable中有一个和项目中需要换肤的原图的名字一模一样的图片,目的就是解析资源,然后达到换肤。
5. 具体代码如下:
1>:SkinAttr:皮肤的属性 比如设置背景、设置文本
/**
* Email: 2185134304@qq.com
* Created by JackChen 2018/4/15 18:06
* Version 1.0
* Params:
* Description: 皮肤的属性 比如设置背景、设置文本
*/
public class SkinAttr {
// 资源的名称
private String mResName ;
// 皮肤的类型,是需要换皮肤还是换背景
private SkinType mSkinType ;
public SkinAttr(String resName, SkinType skinType) {
this.mResName = resName ;
this.mSkinType = skinType ;
}
public void skin(View view) {
mSkinType.skin(view , mResName) ;
}
}
2>:SkinType:皮肤的类型
/**
* Email: 2185134304@qq.com
* Created by JackChen 2018/4/15 18:07
* Version 1.0
* Params:
* Description: 皮肤的类型
*/
public enum SkinType {
TEXT_COLOR("textColor") {
@Override
public void skin(View view, String resName) {
SkinResource skinResource = getSkinResource();
ColorStateList color = skinResource.getColorByName(resName);
if (color == null){
return;
}
TextView textView = (TextView) view;
textView.setTextColor(color);
}
}, BACKGROUND("background") {
@Override
public void skin(View view, String resName) {
// 背景可能是图片,也可能是颜色
SkinResource skinResource = getSkinResource();
Drawable drawable = skinResource.getDrawableByName(resName);
if (drawable != null){
// 背景可能是图片
ImageView imageView = (ImageView) view;
imageView.setBackground(drawable);
}
// 背景可能是颜色
ColorStateList color = skinResource.getColorByName(resName);
if (color != null){
// 这里直接设置默认颜色
view.setBackgroundColor(color.getDefaultColor());
}
}
}, SRC("src") {
@Override
public void skin(View view, String resName) {
// 获取资源设置
SkinResource skinResource = getSkinResource();
Drawable drawable = skinResource.getDrawableByName(resName);
if (drawable != null){
ImageView imageView = (ImageView) view;
imageView.setImageDrawable(drawable);
return;
}
}
};
public SkinResource getSkinResource() {
return SkinManager.getInstance().getSkinResource() ;
}
// 会根据传递的名字调用对应的方法
private String mResName ;
SkinType(String resName){
this.mResName = resName ;
}
public abstract void skin(View view, String resName) ;
public String getResName() {
return mResName;
}
}
3>:SkinView:皮肤的 各种 View,比如Button、TextView、ImageView
/**
* Email: 2185134304@qq.com
* Created by JackChen 2018/4/15 18:05
* Version 1.0
* Params:
* Description: 皮肤的 各种 View,比如Button、TextView、ImageView
*/
public class SkinView {
private View mView ;
private List<SkinAttr> mSkinAttrs ;
public SkinView(View view, List<SkinAttr> skinAttrs) {
this.mView = view ;
this.mSkinAttrs = skinAttrs ;
}
public void skin(){
for (SkinAttr attr : mSkinAttrs) {
attr.skin(mView) ;
}
}
}
4>:ISkinChangeListener:换肤的回调接口
/**
* Email: 2185134304@qq.com
* Created by JackChen 2018/4/21 11:08
* Version 1.0
* Params:
* Description: 换肤的回调接口
*/
public interface ISkinChangeListener {
void changeSkin(SkinResource skinResource) ;
}
5>:SkinConfig:一些配置
/**
* Email: 2185134304@qq.com
* Created by JackChen 2018/4/21 10:00
* Version 1.0
* Params:
* Description:
*/
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;
}
6>:SkinPreUtils:相关工具类
/**
* Email: 2185134304@qq.com
* Created by JackChen 2018/4/21 10:01
* Version 1.0
* Params:
* Description:
*/
public class SkinPreUtils {
private static SkinPreUtils mInstance;
private Context mContext;
/**
* 这里的context必须调用 context.getApplicationContext() 否则内存泄露
*/
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("");
}
}
7>:SkinAppCompatViewInflater:这个是为了兼容 5.0的属性 ,如果不兼容,可能会有一些问题
/**
* Email: 2185134304@qq.com
* Created by JackChen 2018/4/15 18:39
* Version 1.0
* Params:
* Description: 这个是为了兼容 5.0的属性 ,如果不兼容,可能会有一些问题
*/
import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.TypedArray;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.ArrayMap;
import android.support.v4.view.ViewCompat;
import android.support.v7.view.ContextThemeWrapper;
import android.support.v7.widget.AppCompatAutoCompleteTextView;
import android.support.v7.widget.AppCompatButton;
import android.support.v7.widget.AppCompatCheckBox;
import android.support.v7.widget.AppCompatCheckedTextView;
import android.support.v7.widget.AppCompatEditText;
import android.support.v7.widget.AppCompatImageButton;
import android.support.v7.widget.AppCompatImageView;
import android.support.v7.widget.AppCompatMultiAutoCompleteTextView;
import android.support.v7.widget.AppCompatRadioButton;
import android.support.v7.widget.AppCompatRatingBar;
import android.support.v7.widget.AppCompatSeekBar;
import android.support.v7.widget.AppCompatSpinner;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.InflateException;
import android.view.View;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
/**
* This class is responsible for manually inflating our tinted widgets which are used on devices
* running {@link android.os.Build.VERSION_CODES#KITKAT KITKAT} or below. As such, this class
* should only be used when running on those devices.
* <p>This class two main responsibilities: the first is to 'inject' our tinted views in place of
* the framework versions in layout inflation; the second is backport the {@code android:theme}
* functionality for any inflated widgets. This include theme inheritance from it's parent.
*/
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 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) {
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);
}
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) {
// 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('.')) {
// try the android.widget prefix first...
return createView(context, name, "android.widget.");
} 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 (!ViewCompat.hasOnClickListeners(view) || !(context instanceof ContextWrapper)) {
// Skip our compat functionality if: the view doesn't have an onClickListener,
// or the Context isn't a ContextWrapper
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);
}
}
}
8>:SkinAttrSupport:解析皮肤属性的支持类
/**
* Email: 2185134304@qq.com
* Created by JackChen 2018/4/15 18:04
* Version 1.0
* Params:
* Description: 解析皮肤属性的支持类
*/
public class SkinAttrSupport {
/**
* 获取SkinView的属性
*/
public static List<SkinAttr> getSkinAttrs(Context context, AttributeSet attrs) {
// 解析这3个属性 src、background、textColor
List<SkinAttr> skinAttrs = new ArrayList<>() ;
int attrLength = attrs.getAttributeCount();
for (int i = 0; i < attrLength; i++) {
// 比如 android:src="@drawable/image_src"
// 获取名称,就是上边的src 比如是布局文件中的 id、src、style、layout_width、layout_height等等
String attrName = attrs.getAttributeName(i);
// 获取值 ,就是上边的 @drawable/image_src #ffffff
String attrValue = attrs.getAttributeValue(i) ;
// 只获取重要的属性
SkinType skinType = getSkinType(attrName) ;
if (skinType != null){
// 资源名称 目前只有 attrValue 并且是一个 @ 开头,int类型
String resName = getResName(context , attrValue) ;
if (TextUtils.isEmpty(resName)){
// 跳出本次循环
continue;
}
SkinAttr skinAttr = new SkinAttr(resName , skinType) ;
skinAttrs.add(skinAttr) ;
}
}
return skinAttrs;
}
/**
* 获取资源的名称
*/
private static String getResName(Context context, String attrValue) {
// 这里需要判断 值 是否是以 "@" 符号开头
if (attrValue.startsWith("@")){
attrValue = attrValue.substring(1) ;
int resId = Integer.parseInt(attrValue) ;
return context.getResources().getResourceEntryName(resId) ;
}
return null;
}
/**
* 通过名称获取 SkinType
*/
private static SkinType getSkinType(String attrName) {
SkinType[] skinTypes = SkinType.values();
for (SkinType skinType : skinTypes) {
if (skinType.getResName().equals(attrName)){
return skinType ;
}
}
return null;
}
}
9>:SkinManager:皮肤的管理类
/**
* Email: 2185134304@qq.com
* Created by JackChen 2018/4/15 18:03
* Version 1.0
* Params:
* Description: 皮肤的管理类
*/
public class SkinManager {
private Context mContext ;
private static Map<ISkinChangeListener , List<SkinView>> mSkinViews = new HashMap<>() ;
private SkinResource mSkinResource ;
private SkinManager(){}
private volatile static SkinManager mInstance ;
public static SkinManager getInstance() {
if (mInstance == null){
synchronized (SkinManager.class){
if (mInstance == null){
mInstance = new SkinManager() ;
}
}
}
return mInstance ;
}
/**
* 这里使用 getApplicationContext()防止内存泄露
*/
public void init(Context context){
this.mContext = context.getApplicationContext() ;
// 每一次打开皮肤都会到这里来,防止皮肤被任意删除,需要做一些措施
// 1. 获取当前皮肤的路径
String currentSkinPath = SkinPreUtils.getInstance(context).getSkinPath() ;
File file = new File(currentSkinPath) ;
// 如果文件路径不存在,就清空皮肤
if (!file.exists()){
SkinPreUtils.getInstance(context).clearSkinInfo();
return;
}
// 最好做一下,能否获取到包名
String packageName = context.getPackageManager().getPackageArchiveInfo(currentSkinPath , PackageManager.GET_ACTIVITIES).packageName;
if (TextUtils.isEmpty(packageName)){
SkinPreUtils.getInstance(context).clearSkinInfo();
return;
}
// 最好校验一下签名
// 如果文件路径存在,就做一些初始化的工作
mSkinResource = new SkinResource(context , currentSkinPath) ;
}
/**
* 加载皮肤
*/
public int loadSkin(String skinPath) {
// 判断1:如果皮肤不存在,就清空皮肤
File file = new File(skinPath) ;
if (!file.exists()){
// 如果皮肤不存在,就清空皮肤
return SkinConfig.SKIN_FILE_NOEXSIST;
}
// 判断2:最好做一下 能不能获取到包名
String packageName = mContext.getPackageManager().getPackageArchiveInfo(
skinPath, PackageManager.GET_ACTIVITIES).packageName;
if(TextUtils.isEmpty(packageName)){
return SkinConfig.SKIN_FILE_ERROR;
}
// 判读3. 判断当前的皮肤如果一样,就不要换肤
String currentSkinPath = SkinPreUtils.getInstance(mContext).getSkinPath() ;
if (skinPath.equals(currentSkinPath)){
return SkinConfig.SKIN_CHANGE_NOTHING;
}
// 校验签名 在增量更新
// 初始化资源管理
mSkinResource = new SkinResource(mContext , skinPath) ;
// 改变皮肤
changeSkin() ;
return SkinConfig.SKIN_CHANGE_SUCCESS ;
}
/**
* 改变皮肤
*/
private void changeSkin() {
Set<ISkinChangeListener> keys = mSkinViews.keySet() ;
for (ISkinChangeListener key : keys) {
List<SkinView> skinViews = mSkinViews.get(key);
for (SkinView skinView : skinViews) {
skinView.skin();
}
// 通知Activity
key.changeSkin(mSkinResource);
}
}
/**
* 保存当前皮肤状态
*/
private void saveSkinStatus(String skinPath) {
SkinPreUtils.getInstance(mContext).saveSkinPath(skinPath);
}
/**
* 获取当前皮肤资源的管理
*/
public SkinResource getSkinResource() {
return mSkinResource;
}
/**
* 恢复默认
*/
public int restoredDefault() {
// 判断当前有没有皮肤,如果没有皮肤,就不要往下边执行
String currentSkinPath = SkinPreUtils.getInstance(mContext).getSkinPath() ;
if (TextUtils.isEmpty(currentSkinPath)){
return SkinConfig.SKIN_CHANGE_NOTHING;
}
// 当前手机运行好的app的apk路径
String skinPath = mContext.getPackageResourcePath() ;
// 初始化资源管理
mSkinResource = new SkinResource(mContext , skinPath) ;
// 每次加载皮肤后,就保存当前的皮肤,只需要保存传递的路径即可
saveSkinStatus(skinPath) ;
// 改变皮肤
changeSkin();
// 清空皮肤信息
SkinPreUtils.getInstance(mContext).clearSkinInfo();
return SkinConfig.SKIN_CHANGE_SUCCESS;
}
/**
* 通过 Activity获取 SkinView
*/
public List<SkinView> getSkinViews(Activity activity) {
return mSkinViews.get(activity) ;
}
/**
* 注册
*/
public void register(ISkinChangeListener skinChangeListener, List<SkinView> skinViews) {
mSkinViews.put(skinChangeListener , skinViews) ;
}
/**
* 检测是否需要换肤
*/
public void checkChangeSkin(SkinView skinView) {
// 如果当前有皮肤,也就是说保存了皮肤的路径,就去换一下皮肤
String currentSkinPath = SkinPreUtils.getInstance(mContext).getSkinPath() ;
if (!TextUtils.isEmpty(currentSkinPath)){
// 如果当前皮肤路径不为空,就换肤
skinView.skin();
}
}
/**
* 防止内存泄露
*/
public void unregister(ISkinChangeListener skinChangeListener) {
mSkinViews.remove(skinChangeListener) ;
}
}
10>:SkinResource:皮肤的资源管理
/**
* Email: 2185134304@qq.com
* Created by JackChen 2018/4/15 18:04
* Version 1.0
* Params:
* Description: 皮肤的资源管理
*/
public class SkinResource {
// 所有的资源都是通过这个获取 , 主要获取的就是 textName、textColor
private Resources mSkinResources;
private String mPackageName;
public SkinResource(Context context , String skinPath) {
try {
// 读取本地的一个 .skin里面的资源
Resources superRes = context.getResources() ;
// 创建AssetsManager
// 不能直接 new AssetManager() ;
// 通过反射来创建 asset对象
AssetManager asset = AssetManager.class.newInstance() ;
// 添加本地下载好的 资源皮肤,就是复制到 手机目录中的 red.skin
// 不能直接调用 addAssetPath()方法,只能通过反射调用 该方法
// 参数1:表示方法名称 参数2:表示方法里边的参数类型 如果是String path -> String.class int path -> int.class 等等
Method method = AssetManager.class.getDeclaredMethod("addAssetPath" , String.class) ;
method.setAccessible(true); // 设置权限,防止addAssetPath()方法是私有private的
/*// 反射执行addAssetPath()方法 File.separator就和 "/" 是一样的
method.invoke(asset , Environment.getExternalStorageDirectory().getAbsolutePath() +
File.separator + "red.skin") ;*/
// 反射执行addAssetPath()方法 File.separator就和 "/" 是一样的 路径不能写死,只能是传递进来的path
method.invoke(asset , skinPath) ;
mSkinResources = new Resources(asset , superRes.getDisplayMetrics() , superRes.getConfiguration());
// 获取包名
mPackageName = context.getPackageManager().getPackageArchiveInfo(skinPath , PackageManager.GET_ACTIVITIES).packageName;
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 通过名字获取 drawable图片
* @param resName
* @return
*/
public Drawable getDrawableByName(String resName){
try {
// 参数1:资源名称 参数2:资源类型 参数3:包名
int resId = mSkinResources.getIdentifier(resName , "drawable" , mPackageName) ;
Drawable drawable = mSkinResources.getDrawable(resId) ;
return drawable ;
} catch (Exception e) {
e.printStackTrace();
return null ;
}
}
/**
* 通过名字获取颜色
* @param resName
* @return
*/
public ColorStateList getColorByName(String resName){
try {
int resId = mSkinResources.getIdentifier(resName , "color" , mPackageName) ;
ColorStateList color = mSkinResources.getColorStateList(resId) ;
return color ;
} catch (Exception e) {
e.printStackTrace();
return null ;
}
}
}
具体测试如下:
如果是在真正的开发中,皮肤包是直接从服务器中下载,然后把路径传递给loadSkin()方法即可,这里为了测试,就自己直接把 red.skin皮肤包放到手机存储目录下,然后就直接获取该皮肤包的一个apk的路径,然后直接传递给 loadSkin()方法,然后点击换肤就可以把red.skin包中drawable下的图片读取出来替换我们本地的图片,然后达到换肤的目的。