AppCompatActivity是如何创建View的
- Activity通过LayoutInflater解析出XmlLayout相关信息
- LayoutInflater内部维护了一个InflaterFactory对象
- InflaterFactory接口包含了一个onCreateView方法,用于创建View
- 将解析出的Xml信息转为AttributeSet,交给InflaterFactory来createView
- AppCompatActivity中维护了一个AppCompatDelegate对象
- 这个对象既用于处理兼容性工作,也实现了InflaterFactory接口
- 在Activity执行onCreate方法时,会调用installViewFactory,将delegate设置为LayoutInflater的Factory2
LayoutInflater.Factory2和LayoutInflater.Factory
- Factory2是新版本的Factory接口,Factory是旧接口
- 当Factory2存在时,会忽略Factory,反之则使用Factory来创建View
- Factory2是为了兼容旧版本代码和而引入的,通过delegate和factory轻松实现了两套逻辑的切换
自定义LayoutInflaterFactory
在上一章,我们实现了自定义AssetManager和Resources,但不知道在哪里去应用它们
现在我们知道,View是通过InflaterFactory创建的
如果我们能让Factory使用自定义Resources,那么基本就实现了换肤的功能
先上代码,让大家心里有个底
kotlin
package com.android.app
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class HomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
layoutInflater.factory2 = SkinnerInflaterFactory(this)
super.onCreate(savedInstanceState)
val root = layoutInflater.inflate(R.layout.activity_home, null)
setContentView(root)
}
}
kotlin
package com.android.app
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import com.android.library.skinner.SkinnerAssetManager
typealias androidStyleableRes = androidx.appcompat.R.styleable
class SkinnerInflaterFactory(private val activity: AppCompatActivity) : LayoutInflater.Factory2 {
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
val view = activity.delegate.createView(parent, name, context, attrs)
if (view is ImageView) {
skinImageView(view, attrs)
}
return view
}
override fun onCreateView(name: String, context: Context, attrs: AttributeSet) = null
private fun skinImageView(view: ImageView, attrs: AttributeSet) {
val typedArray = activity.obtainStyledAttributes(attrs, androidStyleableRes.AppCompatImageView)
if (typedArray.hasValue(androidStyleableRes.AppCompatImageView_android_src)) {
val srcDrawableId = typedArray.getResourceId(androidStyleableRes.AppCompatImageView_android_src, 0)
val skinDrawable = SkinnerAssetManager.skinDrawable(srcDrawableId)
view.setImageDrawable(skinDrawable)
}
}
}
代码其实非常简单,如果是自己实现的话,以下点需要注意
- InflaterFactory一旦创建,不可再被修改,除非通过反射强制去修改
- InflaterFactory默认是在onCreate方法里创建的,如果我们想使用自定义的,则需在onCreate之前设置
- 由于InflaterFactory是在onCreate方法中设置的,意味着如果想中途换肤,则必须重启Activity甚至Application才会生效
- 如果想让新的InflaterFactory立刻生效,只能通过反射去强制修改,然后再调用setContentView重新加载布局
- InflaterFactory是从零开始创建完整的View,这意味着我们可以去做任何事情,只要不嫌麻烦
- 比如读到name=TextView时,我们可以创建一个Button返回,完成控件替换
- 比如读到name=TextView时,我们可以创建一个AppCompatTextView返回,完成旧控件自动升级
- 当然,创建一个完整的View,而且是Xml中可能出现的所有View,工作量是非常庞大的
- 我们要的只是更换皮肤,即修改部分属性对应的资源,没必要去自己去创建View
- 我们可以调用默认的Factory去创建View,然后再修改我们想要的属性值即可
- 上面代码中用到的
activity.delegate.createView
即是AppCompatActivity的默认Factory - 如果我们没有自定义Factory的话,
activity.delegate
就会成为默认的LayoutInflater.factory2
使用自定义皮肤资源
上面已经给出了自定义皮肤资源的代码
kotlin
typealias androidStyleableRes = androidx.appcompat.R.styleable
private fun skinImageView(view: ImageView, attrs: AttributeSet) {
val typedArray = activity.obtainStyledAttributes(attrs, androidStyleableRes.AppCompatImageView)
if (typedArray.hasValue(androidStyleableRes.AppCompatImageView_android_src)) {
val srcDrawableId = typedArray.getResourceId(androidStyleableRes.AppCompatImageView_android_src, 0)
val skinDrawable = SkinnerAssetManager.skinDrawable(srcDrawableId)
view.setImageDrawable(skinDrawable)
}
}
在这段代码里,我们做了以下工作
- 判断控件类型是不是我们想要修改的
- 找到改控件对应的样式空间,即styleable.namespace
- 找到自己想要修改的属性,即styleable.namespace_attr
- 皮肤包中如果存在该资源,则使用皮肤包中的资源,否则使用安装包中的默认资源
- 以上动态加载资源的过程,是通过SkinnerAssetManager去实现的
十万个为什么
如果只是一个Demo的话,到此为止已经完美实现功能了
但是在实际应用中,我们可能需要支持任意控件,任意属性的修改
这意味着,上一节的代码,可能需要上百段雷同的代码,才能满足所有的要求
并且,哪些属性需要适配换肤功能,Factory也是不知道的,需要我们想办法去指定
理想的情况是,所有资源通过Resources加载,然后根据资源名称对Resources进行Hook
遗憾的是,安卓并未支持以上机制,所以目前已有的皮肤适配方案,都一定程度上依赖手动去配置
下一章,我们将讲解,如何支持全控件全属性适配,并且能够适当简化编码