Android:换肤技术讲解
主题,是许多app必备的一个功能,用户可以根据自己的喜好,来切换具有个性的主题,同时能让我们的app更具把玩性。这篇博文就来聊聊皮肤切换的原理,效果图如下:
这里为了便于理解,在换肤的时候,只是简单切换背景图片,文件颜色和背景色
这篇博文将用到一下知识点:
一、思路
首先通过layoutinflatercompat的setfactory方法设置自定义的layoutinflaterfactory,并实现oncreateview,我们可以在该方法中解析xml的每一个节点(即view ),先通过组件名创建对应的view ,再遍历每一个view的 attrs属性和值,并以map保存,以便后续调用(即皮肤资源的切换)。
我们知道,在android中是通过resource来获取资源的,若能获取插件的resource对象,那么就可以获取其图片等资源,到达换肤的目的,说得简单点,换肤就是换resource和packname。
在拿到插件的resource之后,就可以通过resource id 来给每一个view设置其属性(如background)
当然,道理想必大家都懂,show me your code ~
二、偷梁换柱,换掉应用的layoutinflaterfactory
我们需要持切换皮肤的组件,因此创建skinfactory类实现layoutinflaterfactory接口,并实现该接口中的方法oncreateview
/** * hook you can supply that is called when inflating from a layoutinflater. * you can use this to customize the tag names available in your xml * layout files. * * @param parent the parent that the created view will be placed * in; note that this may be null. * @param name tag name to be inflated. * @param context the context the view is being created in. * @param attrs inflation attributes as specified in xml file. * * @return view newly created view. return null for the default * behavior. */ view oncreateview(view parent, string name, context context, attributeset attrs);
显然这是一个hook, 执行layoutinflater.inflate()的时候调用,如上所述,我们可以通过该方法获取每一个节点的属性和值(即资源id),资源类型(drawable 、color 等)。先简单介绍这四个参数:
parent:即当前节点的父类节点,可能为null name :节点名,列如 textview context :该执行过程的上下文 attrs:该节点的属性集合,例如 background属性那么,我们怎么通过节点来创建对应的组件对象呢?我们都知道在android.widget包下的button在布局文件中的节点名只有button,并不是完整的包路径,例如
以及android.view包下的surfaceview等等。
想必读者明白列举以上的用意了,对,我们需要先对获取到的节点名字进行处理,判断获取到的节点名是组件,还是自定义组件,从而构建完整的class name 。如下代码
private static final string[] prefixlist = { "android.widget.", "android.view.", "android.webkit." }; //这些都是系统组件 @override public view oncreateview(view parent, string name, context context, attributeset attrs) { view view = null; if (name.indexof(".") == -1) { //系统控件 for (string prix : prefixlist) { view = createview(context, attrs, prix + name); if (null != view) { break; } } } else { //自定义控件 view = createview(context, attrs, name); } if (null != view) { parseskinview(view, context, attrs); } return view; }
这里需要我们返回一个view,即该组件对应的view,既然能拿到组件对应的class name,那就好办,直接通过classloader去load一个class即可
//创建一个view private view createview(context context, attributeset attrs, string name) { try { //实例化一个控件 class clarr = context.getclassloader().loadclass(name); constructor constructor = clarr.getconstructor(new class[]{context.class, attributeset.class}); constructor.setaccessible(true); return constructor.newinstance(context, attrs); } catch (classnotfoundexception e) { e.printstacktrace(); } catch (nosuchmethodexception e) { e.printstacktrace(); } catch (illegalaccessexception e) { e.printstacktrace(); } catch (instantiationexception e) { e.printstacktrace(); } catch (invocationtargetexception e) { e.printstacktrace(); } return null; }
在拿到该控件后,需要遍历其需要替换值的属性,例如background,存放在list集合中。
//找到需要换肤的控件 private void parseskinview(view view, context context, attributeset attrs) { list attrlist = new arraylist<>(); for (int i = 0; i < attrs.getattributecount(); i++) { //拿到属性名 string attrname = attrs.getattributename(i); string attrvalue = attrs.getattributevalue(i); int id =-1; string entryname =""; string typename =""; skininterface skininterface = null ; switch (attrname) { case "background"://需要进行换肤 id = integer.parseint(attrvalue.substring(1)); entryname = context.getresources().getresourceentryname(id); typename = context.getresources().getresourcetypename(id); skininterface = new backgroundskin(attrname,id,entryname,typename); break; case "textcolor": id = integer.parseint(attrvalue.substring(1)); entryname = context.getresources().getresourceentryname(id); typename = context.getresources().getresourcetypename(id); skininterface = new textskin(attrname,id,entryname,typename); break; default: break; } if(null != skininterface){ attrlist.add(skininterface); } } skinitem skinitem = new skinitem(attrlist,view); map.put(view,skinitem); //在这里进行应用,判断是皮肤资源还是本地资源 skinitem.apply(); }
为了方便属性的替换,这里用skinitem对象来持有view和view对应的属性集合list。
class skinitem { public skinitem(list attrlist, view view) { this.attrlist = attrlist; this.view = view; } public list attrlist; public view view; //更新组件资源,调用skininterface 的实现类 public void apply() { for (skininterface skininterface : attrlist) { skininterface.apply(view); } } }
在进行皮肤切换的时候,有设置background的,有设置textcolor的,但他们都需要以下参数
组件的属性名称,例如 background 组件引用资源的id (integer 类型) 组件引用资源的名称,例如 app_icon 组件引用资源的类型,例如 drawable所以我们这里可以抽象出一个类skininterface,所有需要换肤的实现类都继承该类
public abstract class skininterface { string attrname; int refid = 0; string attrvaluename; string attrtype; public skininterface(string attrname, int refid, string attrvaluename, string attrtype) { this.attrname = attrname; this.refid = refid; this.attrtype = attrtype; this.attrvaluename = attrvaluename; } /** * 执行具体切换工作 * @param view 作用对象 */ public abstract void apply(view view); }
列如skininterface的继承类 textskin
public class textskin extends skininterface { public textskin(string attrname, int refid, string attrvaluename, string attrtype) { super(attrname, refid, attrvaluename, attrtype); } @override public void apply(view view) { if(view instanceof textview){ textview textview = (textview)view ; textview.settextcolor(skinmanager.getinstance().getcolor(refid)); } } }
还有backgroundskin类的实现
public class backgroundskin extends skininterface{ private static final string tag = "backgroundskin"; public backgroundskin(string attrname, int refid, string attrvaluename, string attrtype) { super(attrname, refid, attrvaluename, attrtype); } @override public void apply(view view) { if("color".equals(attrtype)){ view.setbackgroundcolor(skinmanager.getinstance().getcolor(refid)); }else if("drawable".equals(attrtype)){ view.setbackgrounddrawable(skinmanager.getinstance().getdrawable(refid)); } } }
最后在skinfactory中提供一个更新的方法,来实现资源的替换工作
public void update() { for(view view : map.keyset()){ if(null == view){ continue; } map.get(view).apply(); } }
总之一句话,skinfactory 负责创建view并获取其属性名和值,以及后续的切换资源工作
三、resource的中心枢纽——skinmanager
上一节在讲到皮肤切换具体实现类的时候,涉及到skinmanager对象,他就是resource的主要负责人,负责返回组件所需要的资源。
回想一下,我们是如何在activity中获取资源的?是不是通过getresources().get……方法?显然我们需要获取插件的resource对象,才能拿到插件里的资源,先来看看resource的构造函数
@deprecated public resources(assetmanager assets, displaymetrics metrics, configuration config) { this(null); mresourcesimpl = new resourcesimpl(assets, metrics, config, new displayadjustments()); }
这里需要三个参数?有什么办法,那就给他咯~
首先看assetmanager ,他有两个构造函数,一个是hide的,一个是private的,均不能直接new 出来,这个好办~~
assetmanager assetmanager = assetmanager.class.newinstance();
assetmanager 有一个addassetpath方法可以通过文件路径来加载资源,但也是hide状态,怎么办?easy !反射啦~~
method method = assetmanager.class.getmethod("addassetpath", string.class); method.invoke(assetmanager, path);
这样我们就顺利滴拿到了插件的assetmanager对象,剩下的两个参数就直接使用宿主项目上下文的resource的默认值即可
resources resources = context.getresources(); resources skinresource = new resources(assetmanager, resources.getdisplaymetrics(), resources.getconfiguration());
于是乎,就这样顺利的拿到了插件的resource对象,但是我们还需要获取插件的包名
packagemanager packagemanager = context.getpackagemanager(); packageinfo packageinfo = packagemanager.getpackagearchiveinfo(path, packagemanager.get_activities); string skinpackage = packageinfo.packagename;
获取资源不是通过resource吗,为什么还需要插件的packagename 呢?接着往下看
在获取resource对象后,就可以提供接口给其他类获取资源了,例如获取color
public int getcolor(int refid) { if (null == skinresource) { return refid; } string resname = context.getresources().getresourceentryname(refid); int realid = skinresource.getidentifier(resname, "color", skinpackage); int color; if (android.os.build.version.sdk_int >= android.os.build.version_codes.m) { color = skinresource.getcolor(realid, null); } else { color = skinresource.getcolor(realid); } return color; }
看到这里,想必大家知道packagename的用处了吧?就是获取插件同资源名对应的id,然后再通过插件的id 获取对应的资源,获取drawable同理
public drawable getdrawable(@drawableres int refid) { drawable drawable = contextcompat.getdrawable(context, refid); if (null == skinresource) { return drawable; } string resname = context.getresources().getresourceentryname(refid); int resid = skinresource.getidentifier(resname, "drawable", skinpackage); return skinresource.getdrawable(resid); }
这样skinmanager 就创建完成了
四、创建基类
大家说得好,万物基于…..基类~~,这里我们需要创建一个抽象的skinbaseactivity,凡是需要进行换肤的activity都要继承该类
public abstract class skinbaseactivity extends activity { private skinfactory skinfactory ; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); skinfactory = new skinfactory(); //设置当前activity解析xml的工厂类 layoutinflatercompat.setfactory(getlayoutinflater(),skinfactory );//layoutinflaterfactory } //手动更换皮肤 public void update(){ skinfactory.update(); } }
然后在mainactivity中继承该类,并将skinmanager初始化
public class mainactivity extends skinbaseactivity { private static final string tag = "mainactivity "; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); skinmanager.getinstance().init(this); setcontentview(r.layout.activity_main); } }
在进行皮肤切换的时候执行(要确保file的路径正确,否则会出错)
public void change(view view) { string path = new file(environment.getexternalstoragedirectory(), "skin.apk").getabsolutepath(); skinmanager.getinstance().loadskin(path); update(); }
这里要注意加上权限,并到权限管理中心给该应用读写权限
那么,如何恢复默认皮肤呢?很简单,将skinmanager中的resource,packagename替换为当前应用的即可
public void back(view view) { skinmanager.getinstance().setskinresource(getresources()); update(); }
五、run it!
创建一个module,按照宿主apk的资源名重新建立新的资源即可,选择module,打包成apk,再将apk copy到手机的根目录下,在as中切换到宿主apk,将宿主项目打包到手机,即可
至此,换肤技术就讲解完毕
上一篇: 数据库报错Ora-08103的原因
下一篇: 从计划到操作 超详细银河星空前期拍摄教程