本系列方案3篇:

  1. Android 换肤之资源(Resources)加载(一)
  2. setContentView() / LayoutInflater源码剖析(二)
  3. 换肤结构建立(三) — 本篇

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

android 换肤框架搭建及使用 (3 完结篇)

咱们必须创立view,由于咱们需求经过view上的特点来判别它是否需求”换肤”

那么咱们需求在这儿的时分自己反射创立view[张贴自LayoutInflater源码]

android 换肤框架搭建及使用 (3 完结篇)

这儿看不懂没关系,假如单纯的运用来说 一点也不重要!

运用结构条件

  1. 有一个皮肤包, 在一篇中皮肤包怎么制作我说的很详细了!

android 换肤框架搭建及使用 (3 完结篇)

  1. 将皮肤包放入到手机内存中
  2. 记住读写权限,确保能够正常拜访手机内存中的数据
  3. 引入lib-skin
  4. 在 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

android 换肤框架搭建及使用 (3 完结篇)

  1. 皮肤包中设置需求替换的资源

android 换肤框架搭建及使用 (3 完结篇)

  1. 编写改动特点的办法:

android 换肤框架搭建及使用 (3 完结篇)

4.在SkinReplace中规则需求改动的特点,而且经过反射调用对应办法

android 换肤框架搭建及使用 (3 完结篇)

反射办法:

/*
* 作者:史大拿
* 创立时刻: 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篇:

  1. Android 换肤之资源(Resources)加载(一)
  2. setContentView() / LayoutInflater源码剖析(二)
  3. 换肤结构建立(三) — 本篇