赞
踩
本系列计划3篇:
tips: 本篇只说实现思路,以及使用,具体细节请下载代码查看!
本篇实现效果:
在第一篇中: 我们可以通过这段代码来创建自己的Resource来加载另一个apk中的资源
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创建
第二篇不是最近写的,是很早之前写的.这里正好适合,就当作第二篇来使用!
拦截代码:
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" />
这里我们就可以替换
因为这些属性是经常用的,并且是引用的资源文件中的资源,我想没人需要替换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); }
Application.ActivityLifecycleCallbacks#onActivityCreated() 执行时机为:
我们由第二篇知道,Factory是在super.onCreate()中初始化的,并且Factory只能初始化一次,
在android28之前一般通过反射 LayoutInflater.mFactorySet 属性为false来实现加载我们的Factory
但是android28之后就不行了
那么android28之后版本我们可以通过反射来直接替换掉系统的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; }
我们粘贴出来 AppCompatViewInflater.java的时候,只能创建系统的view
我们必须创建view,因为我们需要通过view上的属性来判断它是否需要"换肤"
那么我们需要在这里的时候自己反射创建view[粘贴自LayoutInflater源码]
这里看不懂没关系,如果单纯的使用来说一点也不重要!
可以想像一下网易云,QQ等大厂的换肤, 点击一个按钮,然后下载一个皮肤包存储到手机中,然后我们去读取这个皮肤包的内容
最终我们只需要生成对应的皮肤包给到后台,然后我们就实现了动态的更换皮肤!
如果你已经将皮肤包放入到了手机内存中,并且已经初始化了SkinManager
那么替换皮肤只需要一行代码:
SkinManager.getInstance().loadSkin("皮肤包的在手机中的路径",Activity);
如果你不想使用皮肤包,那么也只需要一行代码:
SkinManager.getInstance().reset();
现在你已经可以实现
换肤了!
如果还需要其他属性换肤,下面会提到,别急!
在fragment中使用皮肤包只需要注意一点:
在view创建完成的时候调用:
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
SkinManager.getInstance().tryInitSkin(getActivity());
}
这是为了避免第一次初始化的时候加载不到皮肤
其他任何改变都不需要!
不需要任何处理
换肤:
SkinManager.getInstance().loadSkin("皮肤包的在手机中的路径",Activity); // 换肤
恢复默认:
SkinManager.getInstance().reset();
首先我们需要随便自定义一个view
4.在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);
之后调用对应方法即可
例如这样:
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中的资源,如果都不存在,那么就报错
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(Activity);
即可
这个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
原创不易,您的点赞与关注就是对我最大的支持!
本篇结束,耗时15天从框架搭建到一行代码换肤,新年前最后一篇,最后祝大家新年快乐~ 年后见
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。