效果图
本文要实现的功能就是当我下载下来皮肤包进行更换的时候,程序中所有页面的皮肤都要同步切换,并且当进程杀死后,重启程序,更换过的皮肤不能够消失,要可以正常显示
实现思路
1、首先要了解类似美团,QQ音乐,网易云等APP的一键换肤换的到底是什么,其实这些APP中换的皮肤本质上就是一个apk文件,每次从APP上下载一个皮肤,就是下载的一个.apk文件,这个文件是有一个moudle通过build apk的方式生成的一个皮肤包
2、这个皮肤包中有我们需要替换的另外一套资源文件,这里要说明,我们更换皮肤其实换的就是指定控件的文字样式,文字颜色,控件背景,布局文件背景等等,那么我们通过拿到皮肤包中的资源文件放入自己设置的Resources中,作为中转
3、我们的实现方式就是要在APP启动之后,在布局文件加载之前做判断,通过监听每一个acvitity的布局文件的加载(注意动态加载的布局是不可以监听的,所以需要换肤的页面必须要在静态布局文件中声明),判断当前的布局文件需不需要换肤;
4、如果需要,那么我们就要遍历该布局文件中的所有控件,并将每一个控件的属性名作为键,属性值作为值保存起来,然后在监听布局文件加载的地方从app的resource拿控件的type和name,然后去皮肤包拿identifier进行替换资源
本文实现方式
本文只是demo,仅提供核心代码,末尾会奉上demo源码以供参考,我们这里省去网络下载皮肤包的操作,自己制作一个皮肤包放入手机内存卡指定目录(这里为了方便就直接放入内存卡根目录)
![]()
然后我们在APP中点击换肤按钮的时候要去加载这个路径下的皮肤包,获取里边的皮肤资源,并进行批量替换
注意:大型项目中批量替换资源文件是有一套完善的标准,其中要替换的资源名字必须要和原有的资源文件名字相同,不然就会由于在皮肤包中找不到对应的资源文件而替换失败
核心框架代码
编写用于监听布局加载的类
/**
* @author YTF
* 布局监听器
*/
public class SkinFactory implements LayoutInflaterFactory {
private List<SkinView> skinViewList = new ArrayList<>();
private static final String TAG = "YTFSkin";
private static final String[] prxfixList = {
"android.widget.",
"android.view.",
"android.webkit."
};
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
Log.i(TAG, "onCreateView: YTFSkin" + name);
View view = null;
if (name.contains(".")) {
view = creatView(context, attrs, name);
}
for (String pre : prxfixList) {
view = creatView(context, attrs, pre + name);
if (view != null) {
break;
}
}
// 收集需要换肤的控件
if (view != null) {
parseView(context, attrs, view);
// 其中attrs是把xml文件中控件的属性和值以键值对的形式统一存于AttributeSet中
}
return view;
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
public void apply() {
for (SkinView skinView : skinViewList) {
skinView.apply();
}
}
//需要换肤的view集合
class SkinView {
private View view;
private List<SkinItem> list;
public SkinView(View view, List<SkinItem> list) {
this.view = view;
this.list = list;
}
// 换肤开关
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
public void apply() {
for (SkinItem skinItem : list) {
if (skinItem.getAttrName().equals("background")) {
if ("color".equals(skinItem.getTypeName())) {
view.setBackgroundColor(SkinManger.getInstance().getColor(skinItem.getRefId()));
} else if ("drawable".equals(skinItem.getTypeName()) || "mipmap".equals(skinItem.getTypeName())) {
view.setBackground(SkinManger.getInstance().getDrawable(skinItem.getRefId()));
}
}
}
}
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
private void parseView(Context context, AttributeSet attrs, View view) {
List<SkinItem> list = new ArrayList<>();
for (int i = 0; i < attrs.getAttributeCount(); i++) {
// 获取每一个控件下属性的键和值
String attrName = attrs.getAttributeName(i);
String idValue = attrs.getAttributeValue(i);//@mipmap/ic_launcher @0x01214 引用属性获取的是16进制码的ID
Log.i("ytf_idValue", "name: " + idValue + "idValue" + idValue);
if (attrName.contains("background")) {
int id = Integer.parseInt(idValue.substring(1));
String typeName = context.getResources().getResourceTypeName(id);
String entryName = context.getResources().getResourceEntryName(id);
// 注意此处APP中的typeName和entryName一定要与皮肤包中名字相同,不然在皮肤包中就会找不到对应名字的drawable图片
SkinItem skinItem = new SkinItem(attrName, id, entryName, typeName);
list.add(skinItem);
}
}
if (!list.isEmpty()) {
SkinView skinView = new SkinView(view, list);
skinViewList.add(skinView);
skinView.apply();
}
}
//单个皮肤属性
class SkinItem {
String attrName;
int refId;
String entryName;
String typeName;
public SkinItem(String attrName, int refId, String entryName, String typeName) {
this.attrName = attrName;
this.refId = refId;
this.entryName = entryName;
this.typeName = typeName;
}
public String getAttrName() {
return attrName;
}
public void setAttrName(String attrName) {
this.attrName = attrName;
}
public int getRefId() {
return refId;
}
public void setRefId(int refId) {
this.refId = refId;
}
public String getEntryName() {
return entryName;
}
public void setEntryName(String entryName) {
this.entryName = entryName;
}
public String getTypeName() {
return typeName;
}
public void setTypeName(String typeName) {
this.typeName = typeName;
}
}
private View creatView(Context context, AttributeSet attrs, String className) {
View view=null;
try {
Class viewClazz = context.getClassLoader().loadClass(className);
Constructor<? extends View> constructor = viewClazz.getConstructor(new Class[]{
Context.class, AttributeSet.class});
view=constructor.newInstance(context, attrs);
return view;
} catch (Exception e) {
e.printStackTrace();
}
return view;
}
}
编写一个基类
/**
* @author YTF
* 编写基类用于实现换肤功能,实现方式就是监听布局文件的加载,
* 所有需要换肤的acvitity只需要集成此基类变会同样具有换肤功能
*/
public class SkinActivity extends Activity {
SkinFactory skinFactory;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SkinManger.getInstance().setContext(this);
skinFactory=new SkinFactory();
// setContentView(R.layout.activity_skin);
// 添加布局监听器
LayoutInflaterCompat.setFactory(getLayoutInflater(),skinFactory );
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
public void changeSkin(){
skinFactory.apply();
}
}
皮肤资源管理器
**
* 皮肤资源管理器
* 用于加载皮肤,找到就加载,找不到就加载默认layout
*/
public class SkinManger {
private Resources resources;
private static final SkinManger ourInstance = new SkinManger();
private Context context;
private String skinPackge;
public void setContext(Context context) {
this.context = context.getApplicationContext();
}
public static SkinManger getInstance() {
return ourInstance;
}
private SkinManger() {
}
//加载apk中的资源
public void loadSkin() {
String path=context.getDir("skin",
Context.MODE_PRIVATE).getAbsolutePath()+"/skin.apk";
// 拿到外置皮肤apk的包名
PackageManager packageManager=context.getPackageManager();
skinPackge=packageManager.getPackageArchiveInfo(path,PackageManager.GET_ACTIVITIES).packageName;
File file=new File(path);
if (!file.exists()){
return;
}
try {
AssetManager assetManager=AssetManager.class.newInstance();
//系统隐藏api(@hide 通过反射调用)
// 系统隐藏方法,同样通过反射强行调用
Method addAssetPath=assetManager.getClass().getMethod("addAssetPath",String.class);
addAssetPath.invoke(assetManager,path);
resources=new Resources(assetManager,context.getResources().getDisplayMetrics(),context.getResources().getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
}
public int getColor(int id) {
if (resources==null){
return context.getResources().getColor(id);
}
String resType=context.getResources().getResourceTypeName(id);
String resName=context.getResources().getResourceEntryName(id);
int skinId=resources.getIdentifier(resName,resType,skinPackge);
if (skinId==0){
return context.getResources().getColor(id);
}
return resources.getColor(skinId);
}
public Drawable getDrawable(int id) {
if (resources==null){
return ContextCompat.getDrawable(context,id);
}
String resType=context.getResources().getResourceTypeName(id);
String resName=context.getResources().getResourceEntryName(id);
int skinId=resources.getIdentifier(resName,resType,skinPackge);
if (skinId==0){
return ContextCompat.getDrawable(context,id);
}
return resources.getDrawable(skinId);
}
}
GitHub地址: