/ 今日科技快讯 /
近日,京东图书与当当网在京签订战略合作协议,当当官方旗舰店在京东全面上线运营。京东图书与当当网表示,双方将以此为起点,在货品融通、客户服务、整合营销、供应链协作等领域持续深化合作,为消费者带来更多平价好书,推进图书行业繁荣,推进全民阅读建设。
/ 作者简介 /
本篇文章来自史大拿的投稿,文章主要分享了android换肤框架搭建及使用的整个过程,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。
史大拿的博客地址:
https://juejin.cn/user/2251439606079277
/ 回顾 /
在第一篇中,我们可以通过这段代码来创建自己的Resource来加载另一个apk中的资源。文章地址:
https://juejin.cn/post/7182471289524158523
try (
// 创建AssetManager
AssetManager assetManager = AssetManager.class.newInstance()
) {
// 反射调用 创建AssetManager#addAssetPath
Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
// 获取到当前apk在手机中的路径
String path = getApplicationContext().getPackageResourcePath();
/// 反射执行方法
method.invoke(assetManager, path);
// 创建自己的Resources
Resources resources = new Resources(assetManager, createDisplayMetrics(), createConfiguration());
// 根据id来获取图片
Drawable drawable = resources.getDrawable(R.drawable.ic_launcher_background, null);
// 设置图片
mImageView.setImageDrawable(drawable);
} catch (Exception e) {
e.printStackTrace();
}
// 这些关于屏幕的就用原来的就可以
public DisplayMetrics createDisplayMetrics() {
return getResources().getDisplayMetrics();
}
public Configuration createConfiguration() {
return getResources().getConfiguration();
}
在第二篇中,我们分析了setContentView() 加载流程,并且分析了LayoutInflater加载view流程。并且我们知道了如何通过Factory来拦截View创建。文章地址:
https://juejin.cn/post/7135710282227777573
第二篇不是最近写的,是很早之前写的。这里正好适合,就当作第二篇来使用!
拦截代码
class CustomParseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val layoutInflater = LayoutInflater.from(this)
// 如果factory2 == null就创建
if (layoutInflater.factory2 == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, object : LayoutInflater.Factory2 {
// SystemAppCompatViewInflater 是粘贴自系统源码 [AppCompatViewInflater]
val compatInflater = SystemAppCompatViewInflater()
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet,
): View? {
// 在这里就可以拦截view的创建
// Factory创建view
val view = compatInflater.createView(parent, name, context, attrs, false,
true,
true,
false
)
return view
}
...
})
}
// 必须在super 之前
super.onCreate(savedInstanceState)
setContentView(activity_custom_parse)
}
}
/ 项目搭建思路 /
要想达到换肤效果,其实就是加载另一个APK中的资源文件,然后实现替换。现在我们已经知道了如何加载另一个APK中的资源,我们只需要保存起来需要替换的view即可,然后再特定的时机去调用它。
在点击换肤的时候,刷新所有保存的view对象,让它自己去加载另一个APK中的资源即可。首先我们需要规定替换哪些资源。
例如有一个view:
<Button
android:id="@+id/bt1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/global_background"
android:text="@string/global_re_skin"
android:textSize="@dimen/global_def_text_font"
android:textColor="@color/global_text_color" />
这里我们就可以替换:
background
text
textSize
textColor
因为这些属性是经常用的,并且是引用的资源文件中的资源,我想没人需要替换width / height。知道了需要替换哪些资源后,我们就可以在解析view的时候来保存起来这些属性,然后在某个时机的时候手动刷新即可。
整个框架搭建我是采用的 Application.ActivityLifecycleCallbacks 这个类可以监听到activity所有的生命周期。并且采用了观察者设计模式、单例等设计模式来实现点击的时候刷新需要改变属性的view。
在使用的时候只需要一行代码就可以搞定。
#Application.java
public void onCreate(){
SkinManager.init(this);
}
在解析属性的时候,我采用了enum的特性。方便解析给view对应属性赋值。例如这样:
public enum SkinReplace {
ANDROID_BACKGROUND("background") {
@Override
void loadResource(View view, SkinAttr attr) {
view.setBackgroundColor(XXX);
}
};
private final String mName;
SkinReplace(String value) {
mName = value;
}
abstract void loadResource(View view, SkinAttr value);
}
/ 框架小细节 /
初始化factory
Application.ActivityLifecycleCallbacks#onActivityCreated()执行时机为:
AppCompatActivity.super.onCreate()之后
setContentView()之前
我们由第二篇知道Factory是在super.onCreate()中初始化的,并且Factory只能初始化一次,在android 28之前一般通过反射LayoutInflater.mFactorySet属性为false来实现加载我们的Factory。但是android 28之后就不行了,那么android 28之后版本我们可以通过反射来直接替换掉系统的Factory即可。
// 通过反射替换掉系统的factory
private SkinLayoutInflaterFactory forceSetFactory2(LayoutInflater inflater, Activity activity) {
Class<LayoutInflater> inflaterClass = LayoutInflater.class;
try {
String mFactoryStr = "mFactory";
Field mFactory = inflaterClass.getDeclaredField(mFactoryStr);
mFactory.setAccessible(true);
String mFactory2Str = "mFactory2";
Field mFactory2 = inflaterClass.getDeclaredField(mFactory2Str);
mFactory2.setAccessible(true);
SkinLayoutInflaterFactory skinLayoutInflaterFactory = new SkinLayoutInflaterFactory(activity);
// 改变factory
mFactory2.set(inflater, skinLayoutInflaterFactory);
mFactory.set(inflater, skinLayoutInflaterFactory);
return skinLayoutInflaterFactory;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
一定创建View成功
我们粘贴出来AppCompatViewInflater.java的时候,只能创建系统的view。
我们必须创建view,因为我们需要通过view上的属性来判断它是否需要“换肤”。那么我们需要在这里的时候自己反射创建view[粘贴自LayoutInflater源码]。
这里看不懂没关系,如果单纯的使用来说,一点也不重要!
/ 换肤使用 /
使用框架前提
1.有一个皮肤包,在一篇中皮肤包如何制作我说的很详细了!
2.将皮肤包放入到手机内存中
3.记得读写权限,保证能够正常访问手机内存中的数据
4.引入lib-skin
5.在Application.onCreate()中初始化SkinManager.init(this)
可以想像一下网易云,QQ等大厂的换肤。点击一个按钮,然后下载一个皮肤包存储到手机中,然后我们去读取这个皮肤包的内容。最终我们只需要生成对应的皮肤包给到后台,然后我们就实现了动态的更换皮肤!
在Activity中换肤
如果你已经将皮肤包放入到了手机内存中,并且已经初始化了SkinManager。那么替换皮肤只需要一行代码:
SkinManager.getInstance().loadSkin("皮肤包的在手机中的路径",Activity);
如果你不想使用皮肤包,那么也只需要一行代码:
SkinManager.getInstance().reset();
现在你已经可以实现:
src
text
text_color
text_size
background
换肤成功了!如果还需要其他属性换肤。下面会提到,别急!
在Fragment中使用换肤
在fragment中使用皮肤包只需要注意一点,在view创建完成的时候调用:
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
SkinManager.getInstance().tryInitSkin(getActivity());
}
这是为了避免第一次初始化的时候加载不到皮肤。其他任何改变都不需要!
在RecyclerView中使用换肤
不需要任何处理。换肤:
SkinManager.getInstance().loadSkin("皮肤包的在手机中的路径",Activity); // 换肤
恢复默认:
SkinManager.getInstance().reset();
/ 换肤方式 /
自定义属性换肤
首先我们需要随便自定义一个view。
皮肤包中设置需要替换的资源。
编写改变属性的方法。
在SkinReplace中规定需要改变的属性,并且通过反射调用对应方法。
反射方法:
/*
* 作者:史大拿
* 创建时间: 1/4/23 8:07 PM
* TODO 自定义反射,反射具体方法属性
* @param view: 需要反射的对象
* @param methodName: 反射的方法名字
* @param SkinReflectionMethod: 反射具体数据 [类型和参数]
*/
public void setCustomAttr(View view, String methodName, SkinReflectionMethod... data) {
try {
Class<?>[] cls = new Class<?>[data.length];
Object[] objects = new Object[data.length];
for (int i = 0; i < data.length; i++) {
cls[i] = data[i].getCls();
objects[i] = data[i].getObj();
}
Method method = view.getClass().getDeclaredMethod(methodName, cls);
method.setAccessible(true);
method.invoke(view, objects);
} catch (Exception e) {
e.printStackTrace();
SkinLog.e("反射失败;" + e.getMessage() + "\t" + SkinConfig.SKIN_ERROR_7);
}
}
到此还是通过以下方法即可换肤。
SkinManager.getInstance().loadSkin("皮肤包的在手机中的路径",Activity);
动态换肤
动态换肤只需要在此方法之后调用对应方法即可。
SkinManager.getInstance().loadSkin("皮肤包的在手机中的路径",Activity);
drwable SkinManager.getInstance().getDrawable(String)
string SkinManager.getInstance().getString(String)
color SkinManager.getInstance().getColor(String)
dimen SkinManager.getInstance().getFontSize(String)
例如这样:
findViewById(R.id.bt_re_skin).setOnClickListener(v -> {
// 换肤
SkinManager.getInstance().loadSkin(PATH,Activity);
mTextView.setBackground(SkinManager.getInstance().getDrawable("global_skin_drawable_background"));
mTextView.setText(SkinManager.getInstance().getString("global_custom_view_text"));
});
如果app中有一个A资源,皮肤包中没有A资源,现在已经换肤了。那么还是默认使用app中的A资源,但是如果app中没有A资源,并且皮肤包中也没有A资源,那么就报错了。就是一句话,如果当前是换肤状态,那么优先使用皮肤包中的资源,如果皮肤包中的资源不存在,则使用app中的资源;如果都不存在,那么就报错。
Dialog换肤
AlertDialog
private AlertDialog alertDialog;
private void showAlertDialog(View v) {
// 避免重复解析皮肤包
if (alertDialog == null) {
View view = getLayoutInflater().inflate(R.layout.item_alert_dialog, null);
alertDialog = new AlertDialog.Builder(this)
.setView(view)
.create();
}
if (!alertDialog.isShowing()) {
alertDialog.show();
}
// 初始化第一次,避免第一次的时候没有换肤效果
SkinManager.getInstance().tryInitSkin(this);
}
dialog换肤也是非常简单,只需要Dialog.show()。的时候去调用以下方法即可。
SkinManager.getInstance().tryInitSkin(this);
DialogFragment换肤
这个dialog当作一个fragment用即可。和fragment注意事项相同,需要当view加载完成的时候在尝试刷新一下。
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
SkinManager.getInstance().tryInitSkin(getActivity());
}
最后一点,换肤只能替换View的属性,因为Factory只能拦截View,不能拦截ViewGroup。完整项目地址
https://gitee.com/lanyangyangzzz/skin-demo
推荐阅读:
Kotlin Flow响应式编程,StateFlow和SharedFlow
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注