本系列方案3篇:
- Android 换肤之资源(Resources)加载(一)
- setContentView() / LayoutInflater源码剖析(二)
- 换肤结构建立(三) — 本篇
tips: 本篇只说完结思路,以及运用,详细细节请下载代码查看!
本篇完结作用:
fragment换肤 | recyclerView换肤 | 自定义view特点换肤 |
---|---|---|
翻开 | 翻开 | 翻开 |
动态换肤 | dialog换肤 | |
翻开 | 翻开 |
回忆
在第一篇中: 咱们能够经过这段代码来创立自己的Resource来加载另一个apk中的资源
try(
// 创立AssetManager
AssetManagerassetManager=AssetManager.class.newInstance()
) {
// 反射调用 创立AssetManager#addAssetPath
Methodmethod=AssetManager.class.getDeclaredMethod("addAssetPath",String.class);
// 获取到当前apk在手机中的路径
Stringpath=getApplicationContext().getPackageResourcePath();
/// 反射履行办法
method.invoke(assetManager,path);
// 创立自己的Resources
Resourcesresources=newResources(assetManager,createDisplayMetrics(),createConfiguration());
// 根据id来获取图片
Drawabledrawable=resources.getDrawable(R.drawable.ic_launcher_background,null);
// 设置图片
mImageView.setImageDrawable(drawable);
}catch(Exceptione) {
e.printStackTrace();
}
// 这些关于屏幕的就用原来的就能够
publicDisplayMetricscreateDisplayMetrics() {
returngetResources().getDisplayMetrics();
}
publicConfigurationcreateConfiguration() {
returngetResources().getConfiguration();
}
在第二篇中: 咱们剖析了setContentView() 加载流程, 而且剖析了LayoutInflater加载view流程
而且咱们知道了怎么经过Factory来阻拦View创立
第二篇不是最近写的,是很早之前写的.这儿正好适合,就当作第二篇来运用!
阻拦代码:
classCustomParseActivity:AppCompatActivity() {
overridefunonCreate(savedInstanceState:Bundle?) {
vallayoutInflater=LayoutInflater.from(this)
// 假如factory2 == null就创立
if(layoutInflater.factory2==null) {
LayoutInflaterCompat.setFactory2(layoutInflater,object:LayoutInflater.Factory2{
// SystemAppCompatViewInflater 是张贴自体系源码 [AppCompatViewInflater]
valcompatInflater=SystemAppCompatViewInflater()
overridefunonCreateView(
parent:View?,
name:String,
context:Context,
attrs:AttributeSet,
):View?{
// 在这儿就能够阻拦view的创立
// Factory创立view
valview=compatInflater.createView(parent,name,context,attrs,false,
true,
true,
false
)
returnview
}
...
})
}
// 必须在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
publicvoidonCreate(){
SkinManager.init(this);
}
在解析特点的时分,我选用了enum的特性 方便解析给view对应特点赋值
例如这样:
publicenumSkinReplace{
ANDROID_BACKGROUND("background") {
@Override
voidloadResource(Viewview,SkinAttrattr) {
view.setBackgroundColor(XXX);
}
};
privatefinalStringmName;
SkinReplace(Stringvalue) {
mName=value;
}
abstractvoidloadResource(Viewview,SkinAttrvalue);
}
结构小细节
初始化factory
Application.ActivityLifecycleCallbacks#onActivityCreated() 履行机遇为:
- AppCompatActivity.super.onCreate() 之后
- setContentView() 之前
咱们由第二篇知道,Factory是在super.onCreate()中初始化的,而且Factory只能初始化一次,
在android28之前一般经过反射 LayoutInflater.mFactorySet 特点为false来完结加载咱们的Factory
可是android28之后就不行了
那么android28之后版别咱们能够经过反射来直接替换掉体系的Factory即可
// 经过反射替换掉体系的factory
privateSkinLayoutInflaterFactoryforceSetFactory2(LayoutInflaterinflater,Activityactivity) {
Class<LayoutInflater>inflaterClass=LayoutInflater.class;
try{
StringmFactoryStr="mFactory";
FieldmFactory=inflaterClass.getDeclaredField(mFactoryStr);
mFactory.setAccessible(true);
StringmFactory2Str="mFactory2";
FieldmFactory2=inflaterClass.getDeclaredField(mFactory2Str);
mFactory2.setAccessible(true);
SkinLayoutInflaterFactoryskinLayoutInflaterFactory=newSkinLayoutInflaterFactory(activity);
// 改动factory
mFactory2.set(inflater,skinLayoutInflaterFactory);
mFactory.set(inflater,skinLayoutInflaterFactory);
returnskinLayoutInflaterFactory;
}catch(Exceptione) {
e.printStackTrace();
}
returnnull;
}
一定创立View成功
咱们张贴出来 AppCompatViewInflater.java的时分,只能创立体系的view
咱们必须创立view,由于咱们需求经过view上的特点来判别它是否需求”换肤”
那么咱们需求在这儿的时分自己反射创立view[张贴自LayoutInflater源码]
这儿看不懂没关系,假如单纯的运用来说 一点也不重要!
运用结构条件
- 有一个皮肤包, 在一篇中皮肤包怎么制作我说的很详细了!
- 将皮肤包放入到手机内存中
- 记住读写权限,确保能够正常拜访手机内存中的数据
- 引入lib-skin
- 在 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
- 皮肤包中设置需求替换的资源
- 编写改动特点的办法:
4.在SkinReplace中规则需求改动的特点,而且经过反射调用对应办法
反射办法:
/*
* 作者:史大拿
* 创立时刻: 1/4/23 8:07 PM
* TODO 自定义反射,反射详细办法特点
* @param view: 需求反射的目标
* @param methodName: 反射的办法名字
* @param SkinReflectionMethod: 反射详细数据 [类型和参数]
*/
publicvoidsetCustomAttr(Viewview,StringmethodName,SkinReflectionMethod...data) {
try{
Class<?>[]cls=newClass<?>[data.length];
Object[]objects=newObject[data.length];
for(inti=0;i<data.length;i++) {
cls[i]=data[i].getCls();
objects[i]=data[i].getObj();
}
Methodmethod=view.getClass().getDeclaredMethod(methodName,cls);
method.setAccessible(true);
method.invoke(view,objects);
}catch(Exceptione) {
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
privateAlertDialogalertDialog;
privatevoidshowAlertDialog(Viewv) {
// 防止重复解析皮肤包
if(alertDialog==null) {
Viewview=getLayoutInflater().inflate(R.layout.item_alert_dialog,null);
alertDialog=newAlertDialog.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
publicvoidonViewCreated(@NonNullViewview,@NullableBundlesavedInstanceState) {
super.onViewCreated(view,savedInstanceState);
SkinManager.getInstance().tryInitSkin(getActivity());
}
最终一点:换肤目前只能替换View的特点,若想要替换viewGroup特点,需求这样写:
#SystemAppCompatViewInflater.java
final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme) {
switch (name) {
case "TextView":
view = createTextView(context, attrs);
verifyNotNull(view, name);
break;
...
case "LinearLayout":
view = new LinearLayout(context.attrs);
break;
}
}
完好项目地址
原创不易,您的点赞与关注便是对我最大的支持!
本篇结束,耗时15天从结构建立到一行代码换肤,新年前最终一篇,最终祝我们新年快乐~ 年后见
本系列方案3篇:
- Android 换肤之资源(Resources)加载(一)
- setContentView() / LayoutInflater源码剖析(二)
- 换肤结构建立(三) — 本篇