【Android】换肤技术讲解
2018-03-01 22:56
162 查看
主题,是许多APP必备的一个功能,用户可以根据自己的喜好,来切换具有个性的主题,同时能让我们的APP更具把玩性。这篇博文就来聊聊皮肤切换的原理,效果图如下:
这里为了便于理解,在换肤的时候,只是简单切换背景图片,文件颜色和组件背景色
这篇博文将用到一下知识点:
classLoader:实例化控件
PackageManager:拿到插件的包信息
反射:拿到插件的resource
LayoutInflaterFactory:解析xml
我们知道,在Android中是通过resource来获取资源的,若能获取插件的resource对象,那么就可以获取其图片等资源,到达换肤的目的,说得简单点,换肤就是换resource和packName。
在拿到插件的resource之后,就可以通过resource Id 来给每一个view设置其属性(如background)
当然,道理想必大家都懂,show me your code ~
显然这是一个hook, 执行LayoutInflater.inflate()的时候调用,如上所述,我们可以通过该方法获取每一个节点的属性和值(即资源id),资源类型(drawable 、color 等)。先简单介绍这四个参数:
parent:即当前节点的父类节点,可能为null
name :节点名,列如 TextView
context :该执行过程的上下文
attrs:该节点的属性集合,例如 background属性
那么,我们怎么通过节点来创建对应的组件对象呢?我们都知道在android.widget包下的Button在布局文件中的节点名只有Button,并不是完整的包路径,例如
以及android.view包下的SurfaceView等等。
想必读者明白列举以上的用意了,对,我们需要先对获取到的节点名字进行处理,判断获取到的节点名是系统组件,还是自定义组件,从而构建完整的class name 。如下代码
这里需要我们返回一个view,即该组件对应的view,既然能拿到组件对应的class name,那就好办,直接通过classloader去load一个class即可
在拿到该控件后,需要遍历其需要替换值的属性,例如background,存放在list集合中。
为了方便属性的替换,这里用SkinItem对象来持有view和view对应的属性集合list。
在进行皮肤切换的时候,有设置background的,有设置textColor的,但他们都需要以下参数
组件的属性名称,例如 background
组件引用资源的id (integer 类型)
组件引用资源的名称,例如 app_icon
组件引用资源的类型,例如 drawable
所以我们这里可以抽象出一个类SkinInterface,所有需要换肤的实现类都继承该类
列如SkinInterface的继承类 TextSkin
还有BackgroundSkin类的实现
最后在SkinFactory中提供一个更新的方法,来实现资源的替换工作
总之一句话,SkinFactory 负责创建view并获取其属性名和值,以及后续的切换资源工作
回想一下,我们是如何在activity中获取资源的?是不是通过getResources().get……方法?显然我们需要获取插件的resource对象,才能拿到插件里的资源,先来看看resource的构造函数
这里需要三个参数?有什么办法,那就给他咯~
首先看AssetManager ,他有两个构造函数,一个是hide的,一个是private的,均不能直接new 出来,这个好办~~
AssetManager 有一个addAssetPath方法可以通过文件路径来加载资源,但也是hide状态,怎么办?easy !反射啦~~
这样我们就顺利滴拿到了插件的AssetManager对象,剩下的两个参数就直接使用宿主项目上下文的resource的默认值即可
于是乎,就这样顺利的拿到了插件的resource对象,但是我们还需要获取插件的包名
获取资源不是通过resource吗,为什么还需要插件的packageName 呢?接着往下看
在获取resource对象后,就可以提供接口给其他类获取资源了,例如获取color
看到这里,想必大家知道packageName的用处了吧?就是获取插件同资源名对应的id,然后再通过插件的id 获取对应的资源,获取drawable同理
这样SkinManager 就创建完成了
然后在MainActivity中继承该类,并将SkinManager初始化
在进行皮肤切换的时候执行(要确保file的路径正确,否则会出错)
这里要注意加上权限,并到权限管理中心给该应用读写权限
那么,如何恢复默认皮肤呢?很简单,将SKinmanager中的resource,packageName替换为当前应用的即可
至此,换肤技术就讲解完毕
源码下载
这里为了便于理解,在换肤的时候,只是简单切换背景图片,文件颜色和组件背景色
这篇博文将用到一下知识点:
classLoader:实例化控件
PackageManager:拿到插件的包信息
反射:拿到插件的resource
LayoutInflaterFactory:解析xml
一、思路
首先通过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; <em>note that this may be null</em>. * @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,并不是完整的包路径,例如
<Button android:layout_width="match_parent" android:layout_height="wrap_content"/>
以及android.view包下的SurfaceView等等。
<SurfaceView android:layout_width="wrap_content" android:layout_height="wrap_content" />
想必读者明白列举以上的用意了,对,我们需要先对获取到的节点名字进行处理,判断获取到的节点名是系统组件,还是自定义组件,从而构建完整的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<? extends View> 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<SkinInterface> 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<SkinInterface> attrList, View view) { this.attrList = attrList; this.view = view; } public List<SkinInterface> 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(); }
这里要注意加上权限,并到权限管理中心给该应用读写权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
那么,如何恢复默认皮肤呢?很简单,将SKinmanager中的resource,packageName替换为当前应用的即可
public void back(View view) { SkinManager.getInstance().setSkinResource(getResources()); upDate(); }
五、run it!
创建一个module,按照宿主apk的资源名重新建立新的资源即可,选择module,打包成apk,再将apk copy到手机的根目录下,在as中切换到宿主apk,将宿主项目打包到手机,即可至此,换肤技术就讲解完毕
源码下载
相关文章推荐
- android手游渠道接入业务+技术全讲解
- Android换肤技术总结
- 12月26日,唐攀网络直播讲解《Android手机底层软硬件结合开发技术》
- Android换肤技术总结
- Android QQ技术分享三(QQ换肤之SkinEngine实现)
- Android换肤技术总结
- Android换肤技术总结
- Android图像常用压缩技术 详细讲解啊
- [置顶] 【Android】开发干货-技术分享之高仿QQ换肤SkinEngine实现
- Android Bluetooth蓝牙技术基础讲解
- 技术小黑屋----深入讲解Android中Activity launchMode
- Android高阶之换肤实现技术思路
- 分享磨砺营马剑威老师讲解-Android开发技术点汇总
- Android换肤技术总结
- Android换肤技术总结
- Android换肤技术总结
- Android中的Intent详细讲解
- Android热补丁技术—dexposed原理简析(手机淘宝采用方案)
- 关于 android 中 postDelayed方法的讲解
- Linux01-企业核心技术之逻辑卷LVM原理深入讲解35