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){...}
}
这样整个换肤的流程就走完了