Android 换肤(全局换肤,部分换肤,字体替换,导航栏替换,自定义view换肤,夜间/日间模式)
Posted 夜辉疾风
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 换肤(全局换肤,部分换肤,字体替换,导航栏替换,自定义view换肤,夜间/日间模式)相关的知识,希望对你有一定的参考价值。
采集
大致流程
-
监听所有activity的生命周期回调
//SkinActivityLifecycle application.registerActivityLifecycleCallbacks(new SkinActivityLifecycle());
-
创建activity的时候自定义布局工厂
//SkinLayoutFactory @Override public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) //activity在创建的时候拿到布局加载器 LayoutInflater layoutInflater = LayoutInflater.from(activity); //创建一个皮肤工厂 SkinLayoutFactory skinLayoutFactory = new SkinLayoutFactory(); //给当前activity的布局加载器添加这个工厂 LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutFactory);
-
在布局工厂中寻找出所有view的可替换皮肤的标签并保存
//SkinAttribute public void load(View view, AttributeSet attributeSet) //……具体寻找标签和保存的操作
具体实现
1. Application
//application中初始化皮肤管理类SkinManager
public class App extends Application
@Override
public void onCreate()
super.onCreate();
SkinManager.getInstance().init(this);
2. SkinManager
//皮肤管理类,用于注册activity的生命周期监听和加载替换皮肤
public class SkinManager extends Observable
private Application application;
//单例
private static class OnSkinManager
private static SkinManager skinManager = new SkinManager();
public static SkinManager getInstance()
return OnSkinManager.skinManager;
/**
* 初始化
*
* @param application 当前app的application对象
*/
public void init(Application application)
this.application = application;
//初始化一个SharedPreferences,用于存储用户使用的皮肤
SkinPreference.init(application);
//初始化皮肤资源类
SkinResources.init(application);
//注册activity的生命周期回调监听
application.registerActivityLifecycleCallbacks(new SkinActivityLifecycle());
3. SkinActivityLifecycle
//activity的生命周期监听,在每一个activity创建的时候会去寻找皮肤资源并保存和替换
public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks
//缓存当前activity使用到的Factory,用于在该activity销毁的时候清除掉使用的Factory
private Map<Activity, SkinLayoutFactory> cacheFactoryMap = new HashMap<>();
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle)
try
//activity在创建的时候拿到布局加载器
LayoutInflater layoutInflater = LayoutInflater.from(activity);
//参考LayoutInflater源码中的字段mFactorySet的作用:
//mFactorySet如果添加过一次会变成true,再次添加LayoutInflater的时候则会抛出异常
//以下处理的目的是为了修改LayoutInflater源码中的字段mFactorySet的状态,使之不抛出异常
//得到字段mFactorySet
Field mFactorysets = LayoutInflater.class.getDeclaredField("mFactorySet");
//设置字段mFactorySet可以被访问
mFactorysets.setAccessible(true);
//设置字段mFactorySet的值为false
mFactorysets.setBoolean(layoutInflater, false);
//创建一个皮肤工厂
SkinLayoutFactory skinLayoutFactory = new SkinLayoutFactory();
//给当前activity的布局加载器添加这个工厂
LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutFactory);
//添加观察者,观察者也可以使用接口代替
SkinManager.getInstance().addObserver(skinLayoutFactory);
//添加缓存,以便于activity在销毁的时候删除观察者,以免造成内存泄漏
cacheFactoryMap.put(activity, skinLayoutFactory);
catch (Exception e)
e.printStackTrace();
@Override
public void onActivityDestroyed(@NonNull Activity activity)
//删除观察者
SkinLayoutFactory skinLayoutFactory = cacheFactoryMap.remove(activity);
//注销观察者
SkinManager.getInstance().deleteObserver(skinLayoutFactory);
4. SkinLayoutFactory
//布局换肤的工厂类,用于采集需要换肤的view
public class SkinLayoutFactory implements LayoutInflater.Factory2, Observer
//系统原生view的路径,属于这些路径的才可以换肤,减少消耗和判断
private static final String[] mClassPrefixList =
"android.widget.",
"android.view.",
"android.webkit.",
;
//获取view的class的构造方法的参数,一个view有多个构造方法,每个构造方法的参数不同
private static final Class[] mConstructorSignature = new Class[]Context.class, AttributeSet.class;
//缓存已经通过反射得到某个view的构造函数,例如textview、button的构造方法,减少内存开销和加快业务流程
private static final HashMap<String, Constructor<? extends View>> mConstructorCache = new HashMap<>();
//view属性处理类
private SkinAttribute skinAttribute;
//初始化的时候去创建SkinAttribute类
public SkinLayoutFactory()
this.skinAttribute = new SkinAttribute();
//在创建view的时候去采集view,这里一个layout.xml文件中的所有view标签都会在创建的时候进入该方法
@Nullable
@Override
public View onCreateView(@Nullable View parent, @NonNull String s, @NonNull Context context, @NonNull AttributeSet attributeSet)
//如果是系统的view,则可以通过全类名得到view
View view = createViewFromTag(s, context, attributeSet);
//如果通过全类名拿不到view,则说明当前view是自定义view
//如果是自定义view则调用createview方法
if (view == null)
view = createView(s, context, attributeSet);
//将当前view的所有参数遍历,拿到符合换肤的参数以及对应的resid
//第一步采集view,在这里已经完成
skinAttribute.load(view, attributeSet);
return view;
/**
* 创建原生view
* @param name 标签名。例如:TextView;Button
* @param context 上下文
* @param attributeSet 标签参数
* @return
*/
private View createViewFromTag(String name, Context context, AttributeSet attributeSet)
//检查当前view是否是自定义的view或者android的新view
//例如:自定义的,com.xxx.xxx.CustormView
//系统的,com.androidx.action.AtionBar
if (name.contains("."))
//如果是自定义的或者是系统view则另做处理
return null;
else
//这里获取原生view
View view = null;
//循环去判断当前view的前缀,例如Layout的前缀是android.widget.
//这里拼接出view的全类名进行反射
//如果通过反射拿到了view,说明当前全类名是正确的
//如果通过反射抛出异常了则说明当前全类名是错误的
//只有通过反射拿到了正确的构造方法才能通过构造方法new出当前view对象
for (int i = 0; i < mClassPrefixList.length; i++)
//拼接如果是原生标签,则去创建,获取到全类名
view = createView(mClassPrefixList[i] + name, context, attributeSet);
if (view != null) //通过全类名拿到了view,直接返回出去
break;
return view;
/**
* 创建一个view
*
* @param name 全类名
* @param context 上下文
* @param attributeSet 标签参数
* @return
*/
private View createView(String name, Context context, AttributeSet attributeSet)
//添加缓存,一个xml中如果有多个重复的view,例如多个textview或者button,则缓存的作用就体现出来了
//只要是相同的view,则不需要每次都去通过反射拿view
Constructor<? extends View> constructor = mConstructorCache.get(name);
//没有缓存的构造方法则创建
if (constructor == null)
try
//通过全类名拿到class对象
Class<? extends View> aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
//获取到当前class对象中的构造方法
constructor = aClass.getConstructor(mConstructorSignature);
//将构造方法缓存起来
mConstructorCache.put(name, constructor);
catch (Exception e)
//如果抛出异常,说明这个全类名不正确,则直接返回null
return null;
//构造方法获取到了
if (null != constructor)
try
//这个操作相当于new 一个对象,new的时候传入构造方法的参数
return constructor.newInstance(context, attributeSet);
catch (Exception e)
//如果抛出异常,说明这个构造方法和传递进来的参数不正确
//一般view的构造方法都有一个是:
//public xxx(Context context, AttributeSet attrs)
return null;
return null;
5. SkinAttribute
//Describe: view的属性处理类,采集view和替换资源
public class SkinAttribute
//需要换肤的属性集合,已经找出来了
private static final List<String> mAttribute = new ArrayList<>();
//需要换肤的view
private List<SkinView> skinViews = new ArrayList<>();
//以下这些事需要换肤的属性,如果自己需要替换那些标签属性,则可以继续添加
static
mAttribute.add("background");
mAttribute.add("src");
mAttribute.add("textColor");
mAttribute.add("drawableLeft");
mAttribute.add("drawableRight");
mAttribute.add("drawableTop");
mAttribute.add("drawableBottom");
/**
* 寻找view的可换肤属性并缓存起来
*
* @param view view
* @param attributeSet 属性
*/
public void load(View view, AttributeSet attributeSet)
List<SkinPain> skinPains = new ArrayList<>();
//先筛选一遍,需要修改属性的才往下走
for (int i = 0; i < attributeSet.getAttributeCount(); i++)
//获取属性名字
String attributeName = attributeSet.getAttributeName(i);
//如果当前属性名字是需要修改的属性则去处理
if (mAttribute.contains(attributeName))
//拿到属性值,@2130968664
String attributeValue = attributeSet.getAttributeValue(i);
//写死的色值,暂时不修改
if (attributeValue.startsWith("#"))
continue;
int resId;
//?开头的是系统参数,如下修改
if (attributeValue.startsWith("?"))
//拿到去掉?后的值。
//强转成int,系统编译后的值为int型,即R文件中的id,例如:?123456
//系统的资源id下只有一个标签,类似于resource标签下的style标签,但是style下只有一个item标签
//所以只拿第一个attrid;
int attrId = Integer.parseInt(attributeValue.substring(1));
//获得资源id
resId = SkinThemeUtils.getResId(view.getContext(), new int[]attrId)[0];
else
//其他正常的标签则直接拿到@color/black中在R文件中的@123456
//去掉@后的值则可以直接通过setColor(int resId);传入
resId = Integer.parseInt(attributeValue.substring(1));
if (resId != 0)
//保存属性名字和对应的id用于换肤使用
SkinPain skinPain = new SkinPain(attributeName, resId);
skinPains.add(skinPain);
//如果当前view检查出来了需要替换的资源id,则保存起来
//上面的循环已经循环出了当前view中的所有需要换肤的标签和redId
if (!skinPains.isEmpty())
SkinView skinView = new SkinView(view, skinPains);
skinViews.add(skinView);
//保存:参数->id
public class SkinPain
String attrubuteName;//参数名
int resId;//资源id
public SkinPain(String attrubuteName, int resId)
this.attrubuteName = attrubuteName;
this.resId = resId;
//保存view与之对应的SkinPain对象
public class SkinView
View view;
List<SkinPain> skinPains;
public SkinView(View view, List<SkinPain> skinPains)
this.view = view;
this.skinPains = skinPains;
制作
- 一个没有java代码的apk包,里面有所有相对应名字的资源文件
- 放到服务器或者手机sd卡中用于加载并替换
替换
注意事项
- 制作好的皮肤包需要先下载到手机sd卡中,也可在app中内置几套默认皮肤
- 换肤需要读写sd卡权限
- 注意内存泄漏问题
1. 加载皮肤包资源文件
//换肤
public void change(View view)
//拿到sd卡中的皮肤包
String path = Environment.getExternalStorageDirectory() + File.separator + "skin_apk_1.apk";
//加载
SkinManager.getInstance().loadSkin(path);
2.loadSkin(String filePath)
public class SkinManager extends Observable
/**
* 加载皮肤,并保存当前使用的皮肤
*
* @param skinPath 皮肤路径 如果为空则使用默认皮肤
*/
public void loadSkin(String skinPath)
//如果传递进来的皮肤文件路径是null,则表示使用默认的皮肤
if (TextUtils.isEmpty(skinPath))
//存储默认皮肤
SkinPreference.getInstance().setSkin("");
//清空皮肤资源属性
SkinResources.getInstance().reset();
else
//传递进来的有皮肤包的文件路径则加载
try
//皮肤包文件不存在
if (!new File(skinPath).exists())
Toast.makeText(application, "文件不存在", Toast.LENGTH_LONG).show();
return;
//反射创建AssetManager
AssetManager assetManager = AssetManager.class.newInstance();
//通过反射得到方法:public int addAssetPath(String path)方法
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
//设置当前方法可以被访问
addAssetPath.setAccessible(true);
//调用该方法,传入皮肤包文件路径
addAssetPath.invoke(assetManager, skinPath);
//得到当前app的Resources
Resources appResource = application.getResources();
//根据当前的显示与配置(横竖屏、语言等)创建皮肤包的Resources
Resources skinResource = new Resources(
assetManager,
appResource.getDisplayMetrics(),
appResource.getConfiguration());
//保存当前用户设置的皮肤包路径
SkinPreference.getInstance().setSkin(skinPath);
//获取外部皮肤包的包名,首先得到PackageManager对象
PackageManager packageManager = application.getPackageManager();
//通过getPackageArchiveInfo得到外部皮肤包文件的包信息
PackageInfo info = packageManager.getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES);
if (info == null)
//一般解析失败的原因有:
//1,没有sd卡权限
//2,皮肤包打包有问题
Toast.makeText(application, "解析皮肤包失败", Toast.LENGTH_LONG).show();
return;
//得到皮肤包包名
String packageName = info.packageName;
//开始设置皮肤
SkinResources.getInstance().applySkin(skinResource, packageName);
catch (Exception e)
e.printStackTrace();
//一下观察者操作可以用接口代替
//通知所有采集的View更新皮肤
setChanged();
//被观察者通知所有观察者
notifyObservers(null);
3. SkinResources
/**
* 皮肤资源类
* 用来加载本地默认的资源或者皮肤包中的资源
*/
public class SkinResources
private static SkinResources instance;
//皮肤包的资源
private Resources mSkinResources;
//皮肤包包名
private String mSkinPkgName;
//是否加载默认的皮肤资源
private boolean isDefaultSkin = trueiOS换肤方案