android 换肤探索(一) 手把手做一个皮肤包

一、原理

普通的开发时,如果要给一个View设置背景颜色,通常会这样

view.setBackgroundColor(context.getResources().getColor(R.color.black));

context.getResources()返回一个Resources对象,里面有getColor(...),getString(...)等方法,可以通过这些方法返回颜色字符串等。那么,如果我们如果能从另一个地方获取Resources对象,例如一个皮肤包,然后读取里面的颜色图片等,是不是就可以换肤了呢。

二、如何获取皮肤包的资源文件

AssetManager提供了这么一个方法,通过path更改Asset路径.该方法是hide的。

    public int addAssetPath(String path) {
        return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);
    }

然后Resources的构造方法是这样的:

    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(null);
        mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
    }

所以,想要通过皮肤包创建一个Resources,需要这样写:

 private Resources getAsseResources(Context context, String skinApkPath) {
        try {
            Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
            AssetManager assetManager = AssetManager.class.newInstance();
            method.invoke(assetManager, skinApkPath);
            return new Resources(assetManager,
                    context.getResources().getDisplayMetrics(),
                    context.getResources().getConfiguration()
            );
        } catch (Exception e) {
            e.printStackTrace();
        } 
        return null;
    }

三、简单的demo

首先,创建一个Hello World!工程,包名为MyAppliaction,这时MainActivity应该有一个hello world的textview
然后再res/values文件下创建一个colors.xml文件,里面添加一个颜色,我这里取名text_color,颜色为红色

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="text_color">#f00</color>
</resources>

再在MainActivity中这个textview设置为text_color的颜色

textView.setTextColor(getResources().getColor(R.color.text_color));

运行后,出现红色的hello world
android 换肤探索(一) 手把手做一个皮肤包_第1张图片

然后,我们来创建皮肤包
创建一个空的工程,直接命名为MyAppliaction2
在这里插入图片描述
同样res/values文件夹下创建colors.xml文件,把颜色改为绿色,name还是text_color不变

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="text_color">#0f0</color>
</resources>

将这个MyAppliaction2项目打包生成apk。这就是一个皮肤包了。然后不用安装,直接放到手机目录。为了省事,我直接放到外部存储的根目录。
android 换肤探索(一) 手把手做一个皮肤包_第2张图片

如上图,app-debug.apk即为皮肤包。
注意,如果皮肤包放在外部存储,主应用(此文为MyAppliaction)一定要添加读写SD卡的权限,并赋予权限。
皮肤包做成了,如何使用这个皮肤包呢。
创建一个工具类SkinUtil.java

public class SkinUtil {
    private static SkinUtil skinUtil;
    private Resources mResources;
    private String mPath;
    private Context mContext;
    private String skinPackageName;

    private SkinUtil(Context context) {
        this.mPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/app-debug.apk";//皮肤包路径。如果放在外部存储,一定要添加外部存储的读写权限
        this.mResources = getAsseResources(context, mPath);
        skinPackageName = getApkInfo(context,mPath);
    }

    public static SkinUtil getInstance(Context context) {
        if (skinUtil == null) {
            skinUtil = new SkinUtil(context);
        }
        skinUtil.mContext = context;
        return skinUtil;
    }

    //获取皮肤包的Resources
    private Resources getAsseResources(Context context, String skinApkPath) {
        try {
            Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
            AssetManager assetManager = AssetManager.class.newInstance();
            method.invoke(assetManager, skinApkPath);
            return new Resources(assetManager,
                    context.getResources().getDisplayMetrics(),
                    context.getResources().getConfiguration()
            );
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public int getColor(int id) {
        //通过本应用R文件的id获取name
        String name = mContext.getResources().getResourceEntryName(id);
        //通过name获取其在皮肤包的R文件中的id
        int identifier = mResources.getIdentifier(name, "color", skinPackageName);
        if (identifier == 0) {
            //如果没有查到,返回当前的应用的颜色
            return mContext.getResources().getColor(id);
        }
        //根据皮肤包的R文件中的id获取颜色
        return mResources.getColor(identifier);
    }


    public Drawable getDrawable(int id) {
        //通过本应用R文件的id获取name
        String name = mContext.getResources().getResourceEntryName(id);
        //通过name获取其在皮肤包的R文件中的id
        int identifier = mResources.getIdentifier(name, "drawable", skinPackageName);
        if (identifier == 0) {
            //如果没有查到,返回当前的应用的颜色
//            return mContext.getResources().getColor(id);
            return null;
        }
        //根据皮肤包的R文件中的id获取颜色
//        return mResources.getColor(identifier);
        return mResources.getDrawable(identifier);
    }

    //通过文件路径解析包名
    public  String getApkInfo(Context context, String apkPath) {
        try {
            PackageManager pm = context.getPackageManager();
            PackageInfo info = pm.getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES);
            if (info != null) {
                ApplicationInfo appInfo = info.applicationInfo;
                return appInfo.packageName;  //得到安装包名称
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

getAsseResources方法就是前文提到的获取皮肤包的资源文件,getColor方法就是获取皮肤包的color,返回一个color的16进制值。然后通过setTextColor设置这个值就可以了。把前文的setTextColor修改成使用SkinUtil的getcolor方法。

textView.setTextColor(SkinUtil.getInstance(this).getColor(R.color.text_color));

在这里插入图片描述
ok,hello world变成了皮肤包的绿色的了。

这里介绍了getcolor的方法,如果是drawable、string原理都是的一样的,通先获取当前资源id对应的name,然后用这个name获取其在皮肤包中的R文件对应的id,然后再返回具体的颜色图片的。

这里有个缺陷,不能处理setContentView(R.layout.XXX)的颜色,而且设置颜色需要把Context.getResources.get...换成SkinUtil.getInstance(context).get..,如果是开发初还可以依次改,如果是一个开发了很久的项目,改的地方太多了。为了解决这个问题,下一篇讲如何用hook修改全局属性。

你可能感兴趣的:(android 换肤探索(一) 手把手做一个皮肤包)