前言
Factory2
是直接繼承于Factory
根蟹,繼續(xù)跟蹤下Factory
的源碼璧榄,比Factory
的功能更加強(qiáng)大漫贞。 當(dāng)我們新建 Activity 的時候督笆,大部分情況是繼承 AppCompatActivity
处渣。提供了向后兼容性伶贰。
本文將深入探索 AppCompatActivity
的 視圖加載,探索將 xml 布局文件中的 TextView
替換成 AppCompatTextView
的全過程罐栈,并由淺入深介紹了Factory2 的一些奇技淫巧幕袱,幫助各位Android 開發(fā)者簡化開發(fā),提高效率悠瞬。
一们豌、Factory2
在 Android 中涯捻,我們經(jīng)常在 xml 文件中書寫布局买决。這些文件被打包進(jìn) app(因為性能原因由 aapt/2 轉(zhuǎn)換為二進(jìn)制 xml)俭尖,并且在運(yùn)行時由 LayoutInflater
加載坦仍。
在 LayoutInflater
中有兩個方法 setFactory
和 setFactory2
苞也,文檔中是這樣描述的:
當(dāng)使用 LayoutInflater 創(chuàng)建 View 的時候乐疆,綁定一個自定義的 factory 實例记靡。不能為 null刘绣,并且只能設(shè)置一次然磷,設(shè)置之后無法修改摄欲,當(dāng) xml 中每一個元素名字被解析的時候調(diào)用轿亮。若 factory 返回一個 View,將被添加到視圖層級中胸墙;若返回 null我注,factory 的下一個默認(rèn)方法
onCreateView(View, String, AttributeSet)
將被調(diào)用。
注意迟隅,Factory2 implements Factory
但骨,所以對于 Api 11+ 的應(yīng)用來說,應(yīng)該使用 setFactory2
智袭。這就相當(dāng)于給了我們介入 xml 中每一個 View 元素的創(chuàng)建過程的機(jī)會奔缠。讓我們看一個實際使用:
上面的代碼中,我們僅僅為當(dāng)前 Context
的 LayoutInflater
設(shè)置了一個 Factory2
吼野。這樣只要發(fā)現(xiàn)了 TextView
校哎,都會被替換為我們自己的實現(xiàn)類 RedTextView
。
RedTextView
是 TextView
的子類瞳步,提供了 setBackgroundColor
方法贬蛙,將背景置為紅色:
布局文件 factory.xml
是這樣的:
運(yùn)行應(yīng)用并使用 Layout Inspector,我們發(fā)現(xiàn)所有的 TextView
都變成了 RedTextView
谚攒。棒極了阳准!
二、Appcompat Activity 和 Factory2
如果把上面的 FactoryActivity
修改為繼承 AppCompatActivity
馏臭,我們會看到 TextView
確實變成了 RedTextView
野蝇。但是我們添加的 Button
仍然是 Button,并沒有變成 AppCompatButton
括儒,這是為什么绕沈?
AppCompatActivity
的 onCreate
方法的前兩行是:
getDelegate()
根據(jù) api 版本的不同返回對應(yīng)的代理類(AppCompatDelegateImplV14
, AppCompatDelegateImplV23
, AppCompatDelegateImplN
等等)。
下一行代碼 delegate.installViewFactory()
帮寻,當(dāng) layoutInflater.getFactory()
為空的時候乍狐,會調(diào)用 setFactory2
。如果不為空固逗,什么都不會做浅蚪。
所以 Button
沒有發(fā)生變化的原因是藕帜,已經(jīng)設(shè)置過了 Factory,導(dǎo)致 AppcompatActivity
自己的 factory 沒有被 install惜傲。
注意洽故,FactoryActivity
的 setFactory2()
方法是在 super.onCreate
之前調(diào)用的。如果不是的話盗誊,當(dāng)父類是 AppcompatActivity
时甚,setFactory2
會拋出異常。因為 AppCompatActivity
設(shè)置了自己的 Factory 哈踱。文檔中是這樣描述的:它不能為空荒适,且只能被設(shè)置一次;在設(shè)置之后开镣,你不能對 Factory 進(jìn)行改變 刀诬。
三、如何兼容 AppCompatActivity 的 Factory2
如何既能使用自己的 Factory2哑子,又能讓 AppCompatActivity
保留自己的 Facotory 呢?下面給出幾種解決方法肌割。
(一)代理給 AppCompatDelegate
在 AppCompatDelegate
內(nèi)部有一個 createView
方法卧蜓,不要和 Factory
、Factory2
的 onCreateView
混淆把敞。
我們僅僅只需要修改 setFactory2
弥奸,將不需要處理的情況代理給 AppCompatDelegate
:
運(yùn)行一下,TextView
變成了 RedTextView
奋早,Button
變成了 AppCompatButton
盛霎,成功!
(二)重寫 viewInflaterClass
我們看一下 AppCompatDelegate
的 createView
方法耽装,當(dāng) AppCompatViewInflater
沒有初始化時愤炸,會通過反射創(chuàng)建。要初始化的類由 R.styleable.AppCompatTheme_viewInflaterClass
指定掉奄,默認(rèn)就是 AppCompatViewInflater
规个。
對 FactoryActivity
的 theme 進(jìn)行如下修改:
就可以讓 AppCompatDelegate
使用我們自定義的 AppCompatViewInflater
的子類 CustomViewInflater
:
Google 的 Material Design Components 實際上就是使用這種方法來將 Button
修改為對應(yīng)的 MaterialButton
,在 這里 可以看到 姓建。
這個方法很強(qiáng)大诞仓,它可以讓你的 App 使用 Material Design Components 這樣的類庫,卻僅僅只需要設(shè)置合適的主題速兔。
注意 AppCompatViewInflater
還提供了一個可以被重寫的 createView()
方法墅拭,用來處理默認(rèn)情況下沒有被處理的新的組件。當(dāng) AppCompatViewInflater
沒有處理特定的組件類型涣狗,就可以使用這個方法谍婉。
(三)自定義 LayoutInflater
第三種方法是重寫 Activity
的 attachBaseContext
舒憾,改寫 ContextThemeWrapper
的 getSystemService
方法,返回自定義的 LayoutInflater
屡萤。自定義的 LayoutInflater
可以重寫 setFactory2
方法珍剑,加入自己的處理邏輯。這個方法是我從 ViewPump 學(xué)到的死陆。
四招拙、一些小細(xì)節(jié)
下面介紹了 AppCompatDelegate 在進(jìn)行視圖加載過程中的幾個小細(xì)節(jié)。
(一)onCreateView
我們希望 Factory2
的 onCreateView
方法直接調(diào)用 createView
(代理給 AppCompatDelegate
那一小節(jié)中提到過) 措译。事實上别凤,的確也是這么做的。但是代碼中還多了一點(diǎn)東西 - 調(diào)用了 callActivityOnCreateView
领虹。在 AppCompatDelegateImplV14
中是這樣的:
看一下 LayoutInflater
的 源碼 , createViewFromTag
嘗試通過 factory 獲取 view 规哪。如果沒有獲取到,會使用 mPrivateFactory
塌衰。如果依舊沒有獲取到诉稍,會通過視圖標(biāo)簽去創(chuàng)建 view 。mPrivateFactory
是在 Activity 中設(shè)置的最疆。
有意思的是杯巨, mPrivateFactory
的作用是解析 fragment
標(biāo)簽。
在 API 14 之前努酸,LayoutInflater
并沒有提供 mPrivateFactory
讓 Activity 可以有個兜底方案來創(chuàng)建 View 服爷。因此,callActivityOnCreateView
在低版本中提供了這一功能获诈。但這現(xiàn)在都沒有關(guān)系了仍源,反正 AppCompat 目前只兼容 Api 14+ 。
另一個有意思的知識點(diǎn)是 Window.Callback 舔涎。Window.Callback
是一個回調(diào)笼踩,讓調(diào)用者可以攔截 key 的分發(fā),面板亡嫌,菜單等等戳表。它讓 AppCompatActivity 可以處理一些特定時間,例如菜單鍵昼伴,返回鍵等匾旭。
(二)createView
總的來說,AppCompatDelegateImplV9
做了兩件事圃郊。首先价涝,創(chuàng)建了 AppCompatViewInflater
或者在 theme 中指定的其他子類。第二持舆,通過 inflater 創(chuàng)建 View 色瘩。
AppCompatViewInflater
的 createView
使用了正確的 Context
(考慮到支持 app:theme
和 android:theme
伪窖,需要對 Context 進(jìn)行包裝),根據(jù)組件名稱創(chuàng)建對應(yīng)的 AppCompat 組件(例如居兆,如果是 TextView
覆山,就調(diào)用 createTextView
方法返回 AppCompatTextView
)。
(三)支持 app:theme
從 Android 5.0 開始泥栖,可以給 View 設(shè)置 app:theme
以覆蓋特定 View 及其子類的屬性簇宽。AppCompat 通過繼承父 View 的 context 在 Android 5.0 之前復(fù)制這一行為。
在 AppCompat 加載 View 之前吧享,它先拿到父 View 的 Context魏割,然后嘗試創(chuàng)建一個 ContextThemeWrapper(android:theme
或者 app:theme
),保證使用正確的 context 來加載組件钢颂。
另外钞它,如果開發(fā)者明確聲明需要在資源中使用矢量圖,AppCompat 在 Android 5.0 之前還提供了 TintContextWrapper
來包裝 Context 殊鞭。
(四)View 的創(chuàng)建和兜底
通過這些信息遭垛,系統(tǒng)已經(jīng)準(zhǔn)備好如何創(chuàng)建 View 了。
遍歷支持的組件列表操灿,對于通用的 View锯仪,如 TextView
, ImageView
,直接生成對應(yīng)的 AppCompat 子類牲尺。如果是未知類型的 View卵酪,將使用正確的 Context 調(diào)用 createView
幌蚊,默認(rèn)返回 null谤碳,但一般會被 AppCompatViewInflater 的子類重寫。
如果這時候 view 仍然是 null溢豆,會檢查 view 的原始 context 是否和父 View 的 context 一致蜒简。這種情況會發(fā)生在子 View 的 android:theme
和 父 View 不一致。
在檢查 android:onClick
之后漩仙,view 就被返回了搓茬。
五、總結(jié)和使用實例
總結(jié)一下队他,AppCompatActivity
通過給 LayoutInflater
設(shè)置 Factory2
來介入 View 的創(chuàng)建過程卷仑,以提供向后兼容性(為組件提供 tint,處理 android:theme
等)麸折。它也保證了可擴(kuò)展性锡凝,開發(fā)者可以進(jìn)行一些定制處理。
除了 Appcompat垢啼,這一技巧被用來完成了更多有意思的事情窜锯。Probe (現(xiàn)已廢棄) 提供了 OvermeasureInterceptor 來記錄 View 的測量次數(shù)张肾,LayoutBoundsInterceptor 來高亮 View 的邊界。
Calligraphy 使用這一技巧方便的為 TextView 添加字體锚扎。它使用了ViewPump 庫吞瞪,在 wiki 中提供了一些可能的使用方式。
最后驾孔,Google 的 Material Components for Android 通過自定義 AppCompatViewInflater
將 Button
替換為 MaterialButton
芍秆。
原文作者:ahmed el-helw