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,那么基本就實現了換膚的功能
先上代碼,讓大家心里有個底
package com.android.appimport android.os.Bundle
import androidx.appcompat.app.AppCompatActivityclass 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)}
}
package com.android.appimport 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.SkinnerAssetManagertypealias androidStyleableRes = androidx.appcompat.R.styleableclass 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) = nullprivate 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
使用自定義皮膚資源
上面已經給出了自定義皮膚資源的代碼
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
遺憾的是,安卓并未支持以上機制,所以目前已有的皮膚適配方案,都一定程度上依賴手動去配置
下一章,我們將講解,如何支持全控件全屬性適配,并且能夠適當簡化編碼