参考了https://blog.csdn.net/jzman/article/details/108612469
以及开源的https://github.com/fengjundev/Android-Skin-Loader
首先,理解一下默认的创建View方式之前做一次拦截,就是接口Factory,Factory2的重写,里面写入自己的创建View的逻辑,
可以直接将创建的View返回,这样就是自定义View创建
并且也可以在某些条件判断下,选择默认的创建方法,也就是部分自定义View
还可以用来对View的属性集做一些事情
// LayoutInflater.java public interface Factory { /** * @param name Tag名称,如TextView * @param context 上下文环境 * @param attrs Xml属性 * * @return View 新创建的View,如果返回null,则Hook无效 */ public View onCreateView(String name, Context context, AttributeSet attrs); } public interface Factory2 extends Factory { // since API 11,多添加了一个参数parent public View onCreateView(View parent, String name, Context context, AttributeSet attrs); }
//过滤后,一部分View使用默认创建
LayoutInflaterCompat.setFactory2(layoutInflater, new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (name.equals("TextView")){
Button button = new Button(MainActivity.this);
button.setText("我被替换成了Button");
return button;
}
AppCompatDelegate compatDelegate = getDelegate();
View view = compatDelegate.createView(parent,name,context,attrs);
return view;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return onCreateView(null,name,context,attrs);
}
});
有了这样的拦截已经可以做很多事情了,比如AndroidStudio中AppCompatActivity中对默认控件的替换等等。。。
当然这里说到的是自定义皮肤,而且以上的拦截有个不足就是只能在创建View也就是最初渲染的时候进行拦截,自定义皮肤不能一加载就整个界面重新绘制,所以还需要动态的修改属性
动态的修改View属性很简单,问题在于知道(1)哪些View需要自定义皮肤,(2)View的哪些属性需要自定义,(3)自定义属性的值可以哪里找
而anroid-skin-loader给出的解决方案是这样的:
问题一:定义一个自定义属性用来标记View是否需要皮肤,在最初的拦截时从View属性集中找到属性集进行解析
问题二:在问题一的基础上找特定的属性类型(支持自定义皮肤的),属性类型是写死的,我感觉如果想要定义再灵活一点的皮肤样式,这里也可以搞自定义属性
问题三:值可以在资源集Resources里面找,默认找的是当前应用的资源集。如何找自定义资源集呢?所以这里就涉及到一个皮肤包(自定义资源集)的概念,很简答,可以理解为一个apk包里面除了res文件夹之外什么都不写(因为用不着),manifest可以这么写
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="cn.feng.skinpackage"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> </application> </manifest>
res中把自定义的资源填上,对应布局文件中资源值的引用,然后打个包,然后涉及到Android有个PackageManager包解析器,还有个AssetManager资源管理者,下面这段代码
PackageManager mPm = context.getPackageManager();
PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
skinPackageName = mInfo.packageName;
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPkgPath);
Resources superRes = context.getResources();
Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
包解析器解析出包名,是用于资源查找的。调用一个AssetManager反射方法(传参是包文件的路径),就加载了自定义资源集。上述代码得出的skinResource实际上是包括默认的资源集以及自定义的资源集。
那么自定义属性的值到底怎么查找呢?首先基于问题一二,确定了要自定义的属性,然后-》属性id-》属性值的引用,用这个引用和之前解析出的包名(哦,所以如果要多个皮肤,打包的时候注意包名不能重复)在资源集中查找属于资源包的资源值
回到最初的默认View创建前拦截上,这里要做的工作就是解析并保存问题一和问题二的答案,附带的创建View,如果资源是自定义资源集的还要去自定义中找。
问题来了,拦截时自定义资源集还没加载怎么办?所以为了保证加载这一步更快,要把它写在Application创建的时候,还有就是要保存一些配置啦,是否自定义,当前自定义皮肤包的路径了,方便一上来就加载自定义资源(好像是废话)
另外,这个框架也支持动态添加的View(非布局文件中的)的皮肤自定义,想想上文所述,无非是在”答案“中再添加一些数据。
自定义皮肤可以理解为View的动态属性值全套替换方案