Bootstrap

Android 插件式换肤实现

写在前头

    Android的换肤机制有不少,通过加载不同资源文件进行换肤,通过不同的Style文件进行换肤,但是最主流的还是插件式换肤,将资源文件打成一个包,通过AssetManager去加载这个包中的资源文件来换肤。

 

换肤代码

public class SkinManager {
    private SkinManager(){
    }

    private static SkinManager mInstance;

    public Resources getmResource() {
        return mResource;
    }

    private Resources mResource;
    private String mSkinName;
    private String mSkinPath;
    private static Context mContext;
    private String mPackageName;
    private boolean IsThemeExists = false;

    public static SkinManager getInstance(Context context){
        if(mInstance==null){
            mInstance = new SkinManager();
            mContext = context;
        }
        return mInstance;
    }

    public void initSkinName(String path){
        this.mSkinName = path;
        mSkinPath = mContext.getFilesDir()+ "/Skins/"+ mSkinName;
        File file = new File(mSkinPath);
        if(TextUtils.isEmpty(mSkinName)||!file.exists()){
            mResource = mContext.getResources(); //使用默认资源
            mPackageName = mContext.getPackageName();
            return;
        }
        try {
            //读取本地皮肤资源
            Resources superRes = mContext.getResources();
            //通过反射创建AssetManger
            AssetManager asset = AssetManager.class.newInstance();
            //添加本地下载好的皮肤
            @SuppressLint("PrivateApi")
            Method method = AssetManager.class.getDeclaredMethod("addAssetPath",String.class);
            method.invoke(asset, mSkinPath);
            mResource = new Resources(asset,superRes.getDisplayMetrics(),superRes.getConfiguration());
            // 获取skinPath包名
            PackageManager pm = mContext.getPackageManager();
            PackageInfo packageInfo = pm.getPackageArchiveInfo(
                    mSkinPath, PackageManager.GET_ACTIVITIES);
            mPackageName = packageInfo.packageName;
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (Exception e){
            e.printStackTrace();
        }
        IsThemeExists = true;
    }

    /**
     * 通过名字获取Drawable
     * @param resName
     * @return
     */
    public Drawable getDrawableByName(String resName){
        try {
            int resId = mResource.getIdentifier(resName, "drawable", mPackageName);
            return mResource.getDrawable(resId);
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 通过名字获取mipmap
     * @param resName
     * @return
     */
    public Drawable getMipmapByName(String resName){
        try {
            int resId = mResource.getIdentifier(resName, "mipmap", mPackageName);
            return mResource.getDrawable(resId);
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 通过名字获取资源id
     * @param defType 资源类型
     * @param resName 资源名字
     * @return
     */
    public int getResIdByName(String defType,String resName){
        try {
            return mResource.getIdentifier(resName, defType, mPackageName);
        }catch (Exception e){
            e.printStackTrace();
            return -1;
        }
    }

    /**
     * 通过名字获取颜色
     * @param resName
     * @return -1为color不存在
     */
    public Integer getColorByName(String resName){
        try {
            int resId = mResource.getIdentifier(resName, "color", mPackageName);
            return mResource.getColor(resId);
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }
    }

    public boolean judgeThemeExists(){
        return IsThemeExists;
    }
}

 

资源文件打包方法

(1)新建一个不带有activity的android项目

 

(2)将资源文件放入这个项目之中

    这里有两种方法来加载包里的资源文件,通过资源文件名直接加载,或者通过创建一个类来返回资源文件的方法加载。后者更便于修改资源文件名称,前者要求资源文件名称必须相同,但是更为简便。各有各的优势,看自己的取舍了。通过第二种方法加载的话我们需要修改一下代码,需要去加载类而不是加载资源文件。

 

皮肤包中的资源工具类

public class UIUtil {  
      
    public static String getTextString(Context ctx){  
        return ctx.getResources().getString(R.string.app_name);  
    }  
      
    public static Drawable getImageDrawable(Context ctx){  
        return ctx.getResources().getDrawable(R.drawable.ic_launcher);  
    }  
      
    public static View getLayout(Context ctx){  
        return LayoutInflater.from(ctx).inflate(R.layout.activity_main, null);  
    }  
      
    public static int getTextStringId(){  
        return R.string.app_name;  
    }  
      
    public static int getImageDrawableId(){  
        return R.drawable.ic_launcher;  
    }  
      
    public static int getLayoutId(){  
        return R.layout.activity_main;  
    }  
  
}  


修改后的加载类的方法

  try {
        // 获取插件Apk的AssetManager对象
        AssetManager assetManager = PluginUtils
                .getPluginAssetManager(file);
        // 获取插件Apk的Resources对象
        Resources resources = PluginUtils.getPluginResources(
                assetManager, this.getResources().getDisplayMetrics(),
                this.getResources().getConfiguration());
        // 类加载器
        DexClassLoader dexClassLoader = new DexClassLoader(
                file.getAbsolutePath(), this.getDir(skinName,
                Context.MODE_PRIVATE).getAbsolutePath(), null,
                this.getClassLoader());
        // 反射拿到R.drawable类的字节码文件对象
        Class<?> c = dexClassLoader.loadClass(skinPackageName
                + ".R$drawable");
        Field[] fields = c.getDeclaredFields();
        for (Field field : fields) {
            if (field.getName().equals("img_night")) {
                int imgId = field.getInt(R.drawable.class);
                Drawable background = resources.getDrawable(imgId);
                container.setBackgroundDrawable(background);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

 

(3)打包

    这里打包不管是不是签名包都是可以的,然后将打好的包放入手机指定路径中,就可以加载了。一般会把这些包保存到服务器,让用户选择喜欢的皮肤并进行下载即可。

 

;