一 前言
Android的换肤技术已经是很久之前就已经被成熟使用的技术了,然而我最近才在学习和接触热修复的时候才看到,在看了一些换肤的方法之后,并且对市面上比较认可的Android的皮肤,装载机换肤框架的源码进行了分析总结。再次记录一下祭奠自己逝去的时间。
二 换肤介绍
换肤本质上是对资源的一中替换包括,字体,颜色,背景,图片,大小等等。当然这些我们都有成熟的API可以通过控制代码逻辑做到。比如查看的修改背景颜色setBackgroundColor
,TextView中的setTextSize
。修改字体等等但是作为程序员我们怎么能忍受对每个页面的每个元素一个行行代码做换肤处理呢?我们需要用最少的代码实现最容易维护和使用效果完美(动态切换,及时生效)的换肤框架。
1.换肤方式一:切换使用主题主题
使用相同的资源ID,但在不同的主题下边自定义不同的资源。我们通过主动切换到不同的主题从而切换界面元素创建时使用的资源。这种方案的代码量不多发,而且有个很明显的缺点不支持已经创建界面的换肤,必须重新加载界面元素.GitHub Demo
2. 换肤方式二:加载资源包
加载资源包是各种应用程序都在使用的换肤方法,例如我们最常用的输入法皮肤,浏览器皮肤等等。我们可以将皮肤的资源文件放入安装包内部,也可以进行下载缓存到磁盘上.Android的应用程序可以使用这种方式进行换肤.GitHub上面有一个开始非常高的换肤框架Android的皮肤下载器就是通过加载资源包对应用程序进行换肤。对这个框架的分析这个也是这篇文章主要的讲述内容。
对比一下发现切换主题可以进行小幅度的换肤设置(比如某个自定义组件的主题),而如果我们想要对整个应用程序做主题切换那么通过加载资源包的这种方式目前应该说是比较好的了。
三 Android的换肤知识点
1. 换肤相应的API
我们先来看一下Android的提供的一些基本的API,通过使用这些API可以在应用程序内部进行资源对象的替换。
公共类资源{
public String getString(int id)throws NotFoundException {
CharSequence res = mAssets.getResourceText(id);
if(res!= null){
返回资源;
}
抛出新的NotFoundException(“字符串资源ID#0x”
+ Integer.toHexString(id));
}
public Drawable getDrawable(int id)throws NotFoundException {
/ ********部分代码省略******* /
}
public int getColor(int id)throws NotFoundException {{
/ ********部分代码省略******* /
}
/ ********部分代码省略******* /
}
这个是我们常用的资源类的API,我们通常可以使用在资源文件中定义的@+id字符串类型,然后在编译出的R.java中对应的资源文件生产的编号(INT类型),从而通过这个ID(INT类型)调用资源提供的这些API获取到对应的资源对象。这个在同一个应用程序下没有任何问题,但是在皮肤包中我们怎么获取这个ID值呢。
公共类资源{
/ ********部分代码省略******* /
/ **
*通过给的资源名称返回一个资源的标识id。
* @paramname描述资源的名称
* @ paramdefType资源的类型
* @paramdefPackage包名
*
* @返回返回资源ID,0标识未找到该资源
* /
public int getIdentifier(String name,String defType,String defPackage){
if(name == null){
抛出新的NullPointerException(“name is null”);
}
尝试{
return Integer.parseInt(name);
} catch(例外e){
// 忽视
}
return mAssets.getResourceIdentifier(name,defType,defPackage);
}
}
资源提供了可以通过@+id,类型,PACKAGENAME这三个参数就可以在AssetManager中寻找相应的软件包名中有没有输入类型并且ID值都能与参数对应上的ID,进行返回。然后我们可以通过这个ID再调用资源的获取资源的API就可以得到相应的资源。
我们这里需要注意的一点一的英文getIdentifier(String name, String defType, String defPackage)方法状语从句:getString(int id)方法所调用资源对象的mAssets对象必须是同一个,并且包含有PACKAGENAME这个资源包。
2.AssetManager构造
怎么构造一个包含特定的packageName资源的AssetManager对象实例呢?
public final class AssetManagerimplements AutoCloseable {
/ ********部分代码省略******* /
/ **
*创建仅包含基本系统资产的新AssetManager。
*应用程序通常不会使用此方法,而是检索
* {@ linkResources#getAssets}的适当资产经理。不是为了
*由应用程序使用。
* {@hide}
* /
public AssetManager(){
synchronized(this){
if(DEBUG_REFS){
mNumRefs = 0;
incRefsLocked(this.hashCode());
}
INIT(假);
if(localLOGV)Log.v(TAG,“新资产经理:”+这个);
ensureSystemAssets();
}
}
从AssetManager构造的函数来看有{@hide}的朱姐,所以在其他类里面是直接创建AssetManager实例。但是不要忘记的Java中还有反射机制可以创建类对象。
1
AssetManager assetManager = AssetManager.class.newInstance();
让创建的assetManager包含特定的PACKAGENAME的资源信息,怎么办?我们在AssetManager中找到相应的API可以调用。
public final class AssetManagerimplements AutoCloseable {
/ ********部分代码省略******* /
/ **
*向资产经理添加一组额外资产。这可以
*目录或ZIP文件。不适用于应用程序。返回
*添加资产的cookie,或失败时为0。
* {@hide}
* /
public final int addAssetPath(String path){
synchronized(this){
int res = addAssetPathNative(path);
if(mStringBlocks!= null){
makeStringBlocks(mStringBlocks);
}
返回资源;
}
}
}
同样改方法也不支持外部调用,我们只能通过反射的方法来调用。
/ **
* apk路径
* /
String apkPath = Environment.getExternalStorageDirectory()+“/ skin.apk”;
AssetManager assetManager = null;
尝试{
AssetManager assetManager = AssetManager.class.newInstance();
AssetManager.class.getDeclaredMethod(“addAssetPath”,String.class).invoke(assetManager,apkPath);
} catch(Throwable th){
th.printStackTrace();
}
至此我们可以构造属于自己换肤的资源了。
3.换肤资源构造
public Resources getSkinResources(Context context){
/ **
*插件apk路径
* /
String apkPath = Environment.getExternalStorageDirectory()+“/ skin.apk”;
AssetManager assetManager = null;
尝试{
AssetManager assetManager = AssetManager.class.newInstance();
AssetManager.class.getDeclaredMethod(“addAssetPath”,String.class).invoke(assetManager,apkPath);
} catch(Throwable th){
th.printStackTrace();
}
返回新资源(assetManager,context.getResources()。getDisplayMetrics(),context.getResources()。getConfiguration());
}
4.使用资源包中的资源换肤
我们将上述所有的代码组合在一起就可以实现,使用资源包中的资源对应用程序进行换肤。
public Resources getSkinResources(Context context){
/ **
*插件apk路径
* /
String apkPath = Environment.getExternalStorageDirectory()+“/ skin.apk”;
AssetManager assetManager = null;
尝试{
AssetManager assetManager = AssetManager.class.newInstance();
AssetManager.class.getDeclaredMethod(“addAssetPath”,String.class).invoke(assetManager,apkPath);
} catch(Throwable th){
th.printStackTrace();
}
返回新资源(assetManager,context.getResources()。getDisplayMetrics(),context.getResources()。getConfiguration());
}
@覆盖
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”));
/ **
*获取文本资源
* /
int color = resources.getColor(resources.getIdentifier(“night_color”,“color”,“com.tzx.skin”));
imageView.setImageDrawable(绘制);
textView.setText(文本);
}
通过上述介绍,我们可以简单的对当前页面进行换肤了。但是想要做出一个一个成熟换肤框架那么仅仅这些还是不够的,提高一下我们的思维高度,如果我们在查看创建的时候就直接使用皮肤资源包中的资源文件,那么这无疑就使换肤更加的简单已维护。
5. LayoutInflater.Factory
我看过一篇前遇见LayoutInflater及工厂文章的这部分可以省略掉。
很幸运的Android给我们在查看生产的时候做修改提供了法门。
公共抽象类LayoutInflater {
/ ***部分代码省略**** /
公共接口工厂{
public View onCreateView(String name,Context context,AttributeSet attrs);
}
public interface Factory2extends Factory {
public View onCreateView(查看父级,字符串名称,上下文上下文,AttributeSet attrs);
}
/ ***部分代码省略**** /
}
我们可以给当前的页面的窗口对象在创建的时候设置工厂,那么在窗口中的视图进行创建的时候就会先通过自己设置的工厂进行创建.Factory方式使用相关状语从句:注意事项请移位到遇见LayoutInflater及工厂,关于工厂的相关知识点尽在其中。
四 Android的皮肤,装载机解析
1. 初始化
- 初始化换肤框架,导入需要换肤的资源包(当前为一个APK文件,其中只有资源文件)。
公共类SkinApplicationextends Application {
public void onCreate(){
super.onCreate();
initSkinLoader();
}
/ **
*必须先调用init
* /
private void initSkinLoader(){
。SkinManager.getInstance()的init(本);
SkinManager.getInstance()负载();
}
}
2.构造换肤对象
导入需要换肤的资源包,并构造换肤的资源实例。
/ **
*在asyc任务中从apk加载资源
* @ paramskinPackagePath皮肤路径apk
* @paramcallback回调通知用户
* /
public void load(String skinPackagePath,final ILoaderListener callback){
新的AsyncTask(){
protected void onPreExecute(){
if(callback!= null){
callback.onStart();
}
};
@覆盖
protected资源doInBackground(String ... params){
尝试{
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();
方法addAssetPath = assetManager.getClass()。getMethod(“addAssetPath”,String.class);
addAssetPath.invoke(assetManager,skinPkgPath);
//构建换肤的资源实例
资源superRes = context.getResources();
资源skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
//存储当前皮肤路径
SkinConfig.saveSkinPath(context,skinPkgPath);
skinPath = skinPkgPath;
isDefaultSkin = false;
return skinResource;
}
return null;
} catch(例外e){
e.printStackTrace();
return null;
}
};
protected void onPostExecute(参考资料结果){
mResources =结果;
if(mResources!= null){
if(callback!= null)callback.onSuccess();
//更新多有可换肤的界面
notifySkinUpdate();
}其他{
isDefaultSkin = true;
if(callback!= null)callback.onFailed();
}
};
} .execute(skinPackagePath);
}
定义基类
换肤页面的基类的通用代码实现基本换肤功能。
public class BaseFragmentActivityextends FragmentActivityimplements ISkinUpdate,IDynamicNewView {
/ ***部分代码省略**** /
//自定义LayoutInflater.Factory
private SkinInflaterFactory mSkinInflaterFactory;
@覆盖
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
尝试{
//设置LayoutInflater的mFactorySet为真,表示还未设置mFactory,否则会抛出异常。
Field field = LayoutInflater.class.getDeclaredField(“mFactorySet”);
field.setAccessible(真);
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();
}
}
@覆盖
protected void onResume(){
super.onResume();
//注册皮肤管理对象
。SkinManager.getInstance()连接(本);
}
@覆盖
protected void onDestroy(){
super.onDestroy();
//反注册皮肤管理对象
。SkinManager.getInstance()分离(本);
}
/ ***部分代码省略**** /
}
3.SkinInflaterFactory
SkinInflaterFactory进行查看的创建并对视图进行换肤。
构造查看
公共类SkinInflaterFactoryimplements Factory {
/ ***部分代码省略**** /
public View onCreateView(String name,Context context,AttributeSet attrs){
//读取查看的皮肤:使属性,假的为不需要换肤
//如果不允许进行优化,请简单地跳过它
boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE,SkinConfig.ATTR_SKIN_ENABLE,false);
if(!isSkinEnable){
return null;
}
//创建视图
View view = createView(context,name,attrs);
if(view == null){
return null;
}
//如果视图创建成功,对视图进行换肤
parseSkinAttr(context,attrs,view);
返回视图;
}
//创建视图,类比可以查看LayoutInflater的createViewFromTag方法
private View createView(Context context,String name,AttributeSet attrs){
View view = null;
尝试{
if(-1 == name.indexOf('。')){
if(“查看”.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);
}
李(“即将创造”+名称);
} catch(例外e){
Le(“创建时出错”“+ +名+”:“+ e.getMessage());
view = null;
}
返回视图;
}
}
4.对生产的景观进行换肤
公共类SkinInflaterFactoryimplements Factory {
//存储当前活动中的需要换肤的查看
private List mSkinItems = new ArrayList();
/ ***部分代码省略**** /
private void parseSkinAttr(Context context,AttributeSet attrs,View view){
//当前查看的所有属性标签
List viewAttrs = new ArrayList();
for(int i = 0; i
5.资源获取
通过当前的资源ID,找到对应的资源名称。再从皮肤包中找到该资源名称所对应的资源ID。
公共类SkinManagerimplements ISkinLoader {
/ ***部分代码省略**** /
public int getColor(int resId){
int originColor = context.getResources()。getColor(resId);
//是否没有下载皮肤或者当前使用默认皮肤
if(mResources == null || isDefaultSkin){
return originColor;
}
//根据渣油值获取对应的XML的的@ + ID的字符串类型的值
String resName = context.getResources()。getResourceEntryName(resId);
//更具resName在皮肤包的mResources中获取对应的渣油
int trueResId = mResources.getIdentifier(resName,“color”,skinPackageName);
int trueColor = 0;
尝试{
//根据渣油获取对应的资源值
trueColor = mResources.getColor(trueResId);
} catch(NotFoundException e){
e.printStackTrace();
trueColor = originColor;
}
return trueColor;
}
public Drawable getDrawable(int resId){...}
}
其他
除此之外再增加以下对于皮肤的管理API(下载,监听回调,应用,取消,异常处理,扩展模块等等)。
五 总结
换肤就是这么简单〜!〜!
视频→
hook源码实现阿里无闪烁换肤链接:https://pan.baidu.com/s/1E9gdeeLADBiszUU-DFRWvw