手工打造网易云等海量换肤框架

效果图

本文要实现的功能就是当我下载下来皮肤包进行更换的时候,程序中所有页面的皮肤都要同步切换,并且当进程杀死后,重启程序,更换过的皮肤不能够消失,要可以正常显示

这里写图片描述

实现思路

1、首先要了解类似美团,QQ音乐,网易云等APP的一键换肤换的到底是什么,其实这些APP中换的皮肤本质上就是一个apk文件,每次从APP上下载一个皮肤,就是下载的一个.apk文件,这个文件是有一个moudle通过build apk的方式生成的一个皮肤包
2、这个皮肤包中有我们需要替换的另外一套资源文件,这里要说明,我们更换皮肤其实换的就是指定控件的文字样式,文字颜色,控件背景,布局文件背景等等,那么我们通过拿到皮肤包中的资源文件放入自己设置的Resources中,作为中转
3、我们的实现方式就是要在APP启动之后,在布局文件加载之前做判断,通过监听每一个acvitity的布局文件的加载(注意动态加载的布局是不可以监听的,所以需要换肤的页面必须要在静态布局文件中声明),判断当前的布局文件需不需要换肤;
4、如果需要,那么我们就要遍历该布局文件中的所有控件,并将每一个控件的属性名作为键,属性值作为值保存起来,然后在监听布局文件加载的地方从app的resource拿控件的type和name,然后去皮肤包拿identifier进行替换资源

本文实现方式

本文只是demo,仅提供核心代码,末尾会奉上demo源码以供参考,我们这里省去网络下载皮肤包的操作,自己制作一个皮肤包放入手机内存卡指定目录(这里为了方便就直接放入内存卡根目录)
这里写图片描述
然后我们在APP中点击换肤按钮的时候要去加载这个路径下的皮肤包,获取里边的皮肤资源,并进行批量替换
注意:大型项目中批量替换资源文件是有一套完善的标准,其中要替换的资源名字必须要和原有的资源文件名字相同,不然就会由于在皮肤包中找不到对应的资源文件而替换失败
这里写图片描述

核心框架代码

编写用于监听布局加载的类

/**
 * @author YTF
 * 布局监听器
 */
public class SkinFactory implements LayoutInflaterFactory {
    private List<SkinView> skinViewList = new ArrayList<>();
    private static final String TAG = "YTFSkin";
    private static final String[] prxfixList = {
            "android.widget.",
            "android.view.",
            "android.webkit."
    };

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        Log.i(TAG, "onCreateView: YTFSkin" + name);
        View view = null;
        if (name.contains(".")) {
            view = creatView(context, attrs, name);
        }
        for (String pre : prxfixList) {
            view = creatView(context, attrs, pre + name);
            if (view != null) {
                break;
            }
        }
//        收集需要换肤的控件
        if (view != null) {
            parseView(context, attrs, view);
//            其中attrs是把xml文件中控件的属性和值以键值对的形式统一存于AttributeSet中
        }
        return view;
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public void apply() {
        for (SkinView skinView : skinViewList) {
            skinView.apply();

        }
    }

    //需要换肤的view集合
    class SkinView {
        private View view;
        private List<SkinItem> list;

        public SkinView(View view, List<SkinItem> list) {
            this.view = view;
            this.list = list;
        }

        //        换肤开关
        @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
        public void apply() {
            for (SkinItem skinItem : list) {
                if (skinItem.getAttrName().equals("background")) {
                    if ("color".equals(skinItem.getTypeName())) {
                        view.setBackgroundColor(SkinManger.getInstance().getColor(skinItem.getRefId()));

                    } else if ("drawable".equals(skinItem.getTypeName()) || "mipmap".equals(skinItem.getTypeName())) {
                        view.setBackground(SkinManger.getInstance().getDrawable(skinItem.getRefId()));
                    }
                }
            }

        }
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    private void parseView(Context context, AttributeSet attrs, View view) {
        List<SkinItem> list = new ArrayList<>();
        for (int i = 0; i < attrs.getAttributeCount(); i++) {
//            获取每一个控件下属性的键和值
            String attrName = attrs.getAttributeName(i);
            String idValue = attrs.getAttributeValue(i);//@mipmap/ic_launcher @0x01214 引用属性获取的是16进制码的ID
            Log.i("ytf_idValue", "name: " + idValue + "idValue" + idValue);
            if (attrName.contains("background")) {
                int id = Integer.parseInt(idValue.substring(1));
                String typeName = context.getResources().getResourceTypeName(id);
                String entryName = context.getResources().getResourceEntryName(id);
//                注意此处APP中的typeName和entryName一定要与皮肤包中名字相同,不然在皮肤包中就会找不到对应名字的drawable图片
                SkinItem skinItem = new SkinItem(attrName, id, entryName, typeName);
                list.add(skinItem);

            }
        }
        if (!list.isEmpty()) {
            SkinView skinView = new SkinView(view, list);
            skinViewList.add(skinView);
            skinView.apply();
        }
    }

    //单个皮肤属性
    class SkinItem {
        String attrName;
        int refId;
        String entryName;
        String typeName;

        public SkinItem(String attrName, int refId, String entryName, String typeName) {
            this.attrName = attrName;
            this.refId = refId;
            this.entryName = entryName;
            this.typeName = typeName;
        }

        public String getAttrName() {
            return attrName;
        }

        public void setAttrName(String attrName) {
            this.attrName = attrName;
        }

        public int getRefId() {
            return refId;
        }

        public void setRefId(int refId) {
            this.refId = refId;
        }

        public String getEntryName() {
            return entryName;
        }

        public void setEntryName(String entryName) {
            this.entryName = entryName;
        }

        public String getTypeName() {
            return typeName;
        }

        public void setTypeName(String typeName) {
            this.typeName = typeName;
        }
    }

    private View creatView(Context context, AttributeSet attrs, String className) {
        View view=null;
        try {
            Class viewClazz = context.getClassLoader().loadClass(className);
            Constructor<? extends View> constructor = viewClazz.getConstructor(new Class[]{
                    Context.class, AttributeSet.class});
            view=constructor.newInstance(context, attrs);
            return view;

        } catch (Exception e) {
            e.printStackTrace();
        }

        return view;
    }
}

编写一个基类

/**
 * @author YTF
 * 编写基类用于实现换肤功能,实现方式就是监听布局文件的加载,
 * 所有需要换肤的acvitity只需要集成此基类变会同样具有换肤功能
 */

public class SkinActivity extends Activity {

    SkinFactory skinFactory;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SkinManger.getInstance().setContext(this);
        skinFactory=new SkinFactory();
//        setContentView(R.layout.activity_skin);
//        添加布局监听器
        LayoutInflaterCompat.setFactory(getLayoutInflater(),skinFactory );
    }
    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public void changeSkin(){
        skinFactory.apply();
    }
}

皮肤资源管理器

**
 * 皮肤资源管理器
 * 用于加载皮肤,找到就加载,找不到就加载默认layout
 */
public class SkinManger {
    private Resources resources;
    private static final SkinManger ourInstance = new SkinManger();
    private Context context;
    private String skinPackge;

    public void setContext(Context context) {
        this.context = context.getApplicationContext();
    }

    public static SkinManger getInstance() {
        return ourInstance;
    }

    private SkinManger() {
    }
//加载apk中的资源
    public void loadSkin() {
        String path=context.getDir("skin",
                Context.MODE_PRIVATE).getAbsolutePath()+"/skin.apk";
//        拿到外置皮肤apk的包名
        PackageManager packageManager=context.getPackageManager();
        skinPackge=packageManager.getPackageArchiveInfo(path,PackageManager.GET_ACTIVITIES).packageName;
        File file=new File(path);
        if (!file.exists()){
            return;
        }
        try {
            AssetManager assetManager=AssetManager.class.newInstance();
            //系统隐藏api(@hide 通过反射调用)
//            系统隐藏方法,同样通过反射强行调用
            Method addAssetPath=assetManager.getClass().getMethod("addAssetPath",String.class);
            addAssetPath.invoke(assetManager,path);
            resources=new Resources(assetManager,context.getResources().getDisplayMetrics(),context.getResources().getConfiguration());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public int getColor(int id) {
        if (resources==null){
            return context.getResources().getColor(id);
        }
        String resType=context.getResources().getResourceTypeName(id);
        String resName=context.getResources().getResourceEntryName(id);
        int skinId=resources.getIdentifier(resName,resType,skinPackge);
        if (skinId==0){
            return context.getResources().getColor(id);
        }

        return resources.getColor(skinId);

    }

    public Drawable getDrawable(int id) {
        if (resources==null){
            return ContextCompat.getDrawable(context,id);
        }
        String resType=context.getResources().getResourceTypeName(id);
        String resName=context.getResources().getResourceEntryName(id);
        int skinId=resources.getIdentifier(resName,resType,skinPackge);
        if (skinId==0){
            return ContextCompat.getDrawable(context,id);
        }

        return resources.getDrawable(skinId);

    }
}

这里写图片描述

GitHub地址:

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值