2.3 Android 换肤原理

Android 换肤原理

  • 制作皮肤包,皮肤包相当于一个apk,不过只包含了资源文件
  • 获取到皮肤包的Resource对象
  • 标记需要换肤的View
  • 切换时刷新页面

换肤用的Api

1.通过的Resource获取皮肤包中资源(一般是图片,颜色)的id值
public class Resources {
    /********部分代码省略*******/
    /**
     * 通过给的资源名称返回一个资源的标识id。
     * @param name 描述资源的名称
     * @param defType 资源的类型
     * @param defPackage 包名
     * 
     * @return 返回资源id,0标识未找到该资源
     */
    public int getIdentifier(String name, String defType, String defPackage) {
        if (name == null) {
            throw new NullPointerException("name is null");
        }
        try {
            return Integer.parseInt(name);
        } catch (Exception e) {
            // Ignore
        }
        return mAssets.getResourceIdentifier(name, defType, defPackage);
    }
}
2. AssetManage用于构造获取皮肤包的Resource对象

创建一个包含皮肤包packageName的AssetManage对象实例

AssetManager assetManager = AssetManager.class.newInstance();
/**
 * apk路径
 */
String apkPath = Environment.getExternalStorageDirectory()+"/skin.apk";
AssetManager assetManager = null;
try {
    AssetManager assetManager = AssetManager.class.newInstance();
    AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(assetManager, apkPath);
} catch (Throwable th) {
    th.printStackTrace();
}
3. 创建获取换肤包的Resource实例了:
public Resources getSkinResources(Context context){
    /**
     * 插件apk路径
     */
    String apkPath = Environment.getExternalStorageDirectory()+"/skin.apk";
    AssetManager assetManager = null;
    try {
        AssetManager assetManager = AssetManager.class.newInstance();
        AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(assetManager, apkPath);
    } catch (Throwable th) {
        th.printStackTrace();
    }
    return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
}
4. 使用皮肤包中的资源,对app进行换肤
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ImageView imageView = (ImageView) findViewById(R.id.imageView);
    TextView textView = (TextView) findViewById(R.id.text);
    /**
     * 插件资源对象
     */
    Resources resources = getSkinResources(this);
    /**
     * 获取图片资源
     */
    Drawable drawable = resources.getDrawable(resources.getIdentifier("night_icon", "drawable","com.tzx.skin"));
    /**
     * 获取Color资源
     */
    int color = resources.getColor(resources.getIdentifier("night_color","color","com.tzx.skin"));

    imageView.setImageDrawable(drawable);
    textView.setText(text);

}

Android-Skin-Loader 换肤原理

1.load皮肤包
/** 
 * Load resources from apk in asyc task 
 * @param skinPackagePath path of skin apk 
 * @param callback callback to notify user 
 */
public void load(String skinPackagePath, final ILoaderListener callback) { 
    new AsyncTask<String, Void, Resources>() {
 
        protected void onPreExecute() {
            if (callback != null) {
                callback.onStart();
            }
        };
 
        @Override
        protected Resources doInBackground(String... params) {
            try {
                if (params.length == 1) {
                    String skinPkgPath = params[0];
                     
                    File file = new File(skinPkgPath); 
                    if(file == null || !file.exists()){
                        return null;
                    }
                     
                    PackageManager mPm = context.getPackageManager();
                    //检索程序外的一个安装包文件
                    PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
                    //获取安装包报名
                    skinPackageName = mInfo.packageName;
                    //构建换肤的AssetManager实例
                    AssetManager assetManager = AssetManager.class.newInstance();
                    Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
                    addAssetPath.invoke(assetManager, skinPkgPath);
                    //构建换肤的Resources实例
                    Resources superRes = context.getResources();
                    Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
                    //存储当前皮肤路径
                    SkinConfig.saveSkinPath(context, skinPkgPath);
                     
                    skinPath = skinPkgPath;
                    isDefaultSkin = false;
                    return skinResource;
                }
                return null;
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        };
 
        protected void onPostExecute(Resources result) {
            mResources = result;
 
            if (mResources != null) {
                if (callback != null) callback.onSuccess();
                //更新多有可换肤的界面
                notifySkinUpdate();
            }else{
                isDefaultSkin = true;
                if (callback != null) callback.onFailed();
            }
        };
 
    }.execute(skinPackagePath);
}
2. 换肤页面的基类,设置布局文件加载器LayoutInflate.Fatory

用于在加载布局文件创建View的时候,统计需要换肤的View对象

public class BaseFragmentActivity extends FragmentActivity implements ISkinUpdate, IDynamicNewView{
    
    /***部分代码省略****/
    
    //自定义LayoutInflater.Factory
    private SkinInflaterFactory mSkinInflaterFactory;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        try {
            //设置LayoutInflater的mFactorySet为true,表示还未设置mFactory,否则会抛出异常。
            Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
            field.setAccessible(true);
            field.setBoolean(getLayoutInflater(), false);
            //设置LayoutInflater的MFactory
            mSkinInflaterFactory = new SkinInflaterFactory();
            getLayoutInflater().setFactory(mSkinInflaterFactory);

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } 
        
    }

    @Override
    protected void onResume() {
        super.onResume();
        //注册皮肤管理对象
        SkinManager.getInstance().attach(this);
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        //反注册皮肤管理对象
        SkinManager.getInstance().detach(this);
    }
   
}
3. SkinInflaterFactory 自定义布局文件加载器

在某个界面初始化在加载布局文件中的View的时候,通过自定义的布局文件解析器,创建View。

这里有一个判断,每次创建View的时候,会判断改View的skin:enable属性,如果为false,则不支持换肤,直接返回null,将View的创建的流程交给Activity自己创建。只有shin:enable属性为true的时候,才会自己创建View对象,并且在创建的View的同时,解析支持换肤的属性

public class SkinInflaterFactory implements Factory {

    public View onCreateView(String name, Context context, AttributeSet attrs) {
        //读取View的skin:enable属性,false为不需要换肤
        // if this is NOT enable to be skined , simplly skip it 
        boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
        if (!isSkinEnable){
                return null;
        }
        //创建View
        View view = createView(context, name, attrs);
        if (view == null){
            return null;
        }
        //如果View创建成功,对View进行换肤
        parseSkinAttr(context, attrs, view);
        return view;
    }
    //创建View,类比可以查看LayoutInflater的createViewFromTag方法
    private View createView(Context context, String name, AttributeSet attrs) {
        View view = null;
        try {
            if (-1 == name.indexOf('.')){
                if ("View".equals(name)) {
                    view = LayoutInflater.from(context).createView(name, "android.view.", attrs);
                } 
                if (view == null) {
                    view = LayoutInflater.from(context).createView(name, "android.widget.", attrs);
                } 
                if (view == null) {
                    view = LayoutInflater.from(context).createView(name, "android.webkit.", attrs);
                } 
            }else {
                view = LayoutInflater.from(context).createView(name, null, attrs);
            }

            L.i("about to create " + name);

        } catch (Exception e) { 
            L.e("error while create 【" + name + "】 : " + e.getMessage());
            view = null;
        }
        return view;
    }
}
4. 创建了View之后,解析布局文件中View定义的换肤属性

解析View的属性,在某些属性(比如background,)支持换肤的时候,会将解析到的属性保存在一个SkinItem的对象中,每解析一个View都会生成一个SkinItem对象,然后保存在一个ArrayList<SkinItem> 集合中,在点击切换按钮的时候,会遍历该List,替换资源。

public class SkinInflaterFactory implements Factory {
    //存储当前Activity中的需要换肤的View
    private List<SkinItem> mSkinItems = new ArrayList<SkinItem>();
  
    private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
        //当前View的所有属性标签
        List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();
        
        for (int i = 0; i < attrs.getAttributeCount(); i++){
            String attrName = attrs.getAttributeName(i);
            String attrValue = attrs.getAttributeValue(i);
            
            if(!AttrFactory.isSupportedAttr(attrName)){
                continue;
            }
            //过滤view属性标签中属性的value的值为引用类型
            if(attrValue.startsWith("@")){
                try {
                    int id = Integer.parseInt(attrValue.substring(1));
                    String entryName = context.getResources().getResourceEntryName(id);
                    String typeName = context.getResources().getResourceTypeName(id);
                    //构造SkinAttr实例,attrname,id,entryName,typeName
                    //属性的名称(background)、属性的id值(int类型),属性的id值(@+id,string类型),属性的值类型(color)
                    SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
                    if (mSkinAttr != null) {
                        viewAttrs.add(mSkinAttr);
                    }
                } catch (NumberFormatException e) {
                    e.printStackTrace();
                } catch (NotFoundException e) {
                    e.printStackTrace();
                }
            }
        }
        //如果当前View需要换肤,那么添加在mSkinItems中
        if(!ListUtils.isEmpty(viewAttrs)){
            SkinItem skinItem = new SkinItem();
            skinItem.view = view;
            skinItem.attrs = viewAttrs;

            mSkinItems.add(skinItem);
            //是否是使用外部皮肤进行换肤
            if(SkinManager.getInstance().isExternalSkin()){
                skinItem.apply();
            }
        }
    }
}
5. 在进行换肤的时候,替换资源

通过当前的资源id,找到对应的资源name。再从皮肤包中找到该资源name所对应的资源id。

public class SkinManager implements ISkinLoader{
    /***部分代码省略****/
    public int getColor(int resId){
        int originColor = context.getResources().getColor(resId);
        //是否没有下载皮肤或者当前使用默认皮肤
        if(mResources == null || isDefaultSkin){
            return originColor;
        }
        //根据resId值获取对应的xml的的@+id的String类型的值
        String resName = context.getResources().getResourceEntryName(resId);
        //更具resName在皮肤包的mResources中获取对应的resId
        int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);
        int trueColor = 0;
        try{
            //根据resId获取对应的资源value
            trueColor = mResources.getColor(trueResId);
        }catch(NotFoundException e){
            e.printStackTrace();
            trueColor = originColor;
        }
        
        return trueColor;
    }
    public Drawable getDrawable(int resId){...}
}

这样整个换肤的流程就走完了

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容