相信大家對LayoutInflater都不陌生,它經(jīng)常被用來根據(jù)xml生成View。比較熟悉的方法包括:
- LayoutInflater.from(Context context)
- inflate(@LayoutRes int resource, @Nullable ViewGroup root)
- inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
構(gòu)造方法源碼如下,可見LayoutInflater.from(Context context)等同于context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)。
/**
* Obtains the LayoutInflater from the given context.
*/
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
除了上述方法,我今天想介紹的是相對不常用的兩個(gè)方法草戈。
- setFactory(Factory factory)
- setFactory2(Factory2 factory)
這兩個(gè)方法基本功能一致。系統(tǒng)通過Factory提供了一種hook的方法侍瑟,方便開發(fā)者攔截LayoutInflater創(chuàng)建View的過程唐片。應(yīng)用場景包括1)在XML布局中自定義標(biāo)簽名稱;2)全局替換系統(tǒng)控件為自定義View涨颜; 3)替換app中字體费韭;4)全局換膚等。
Factory與Factory2的區(qū)別
二者都是LayoutInflater類內(nèi)部定義的接口庭瑰。Factory2繼承自Factory接口星持,在API 11(HONEYCOMB)中引入的。Factory2比Factory多增加了一個(gè)onCreateView(View parent, String name, Context context, AttributeSet attrs)弹灭,該方法多了一個(gè)parent钉汗,用來存放構(gòu)建出的View羹令。
Android在v4包中提供了LayoutInflaterCompat來幫助完成兼容性的操作。
-
setFactory(
@NonNull LayoutInflater inflater, @NonNull LayoutInflaterFactory factory)
在API Level 26.1.0中被標(biāo)記為Deprecated损痰,官方推薦使用setFactory2方法 - setFactory2(
@NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory)
Factory接口的定義如下,該接口只有一個(gè)onCreateView方法酒来。
public interface Factory {
/**
* Hook you can supply that is called when inflating from a LayoutInflater.
* You can use this to customize the tag names available in your XML
* layout files.
*
* <p>
* Note that it is good practice to prefix these custom names with your
* package (i.e., com.coolcompany.apps) to avoid conflicts with system
* names.
*
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
public View onCreateView(String name, Context context, AttributeSet attrs);
}
Factory2接口的源碼定義如下卢未。
public interface Factory2 extends Factory {
/**
* Version of {@link #onCreateView(String, Context, AttributeSet)}
* that also supplies the parent that the view created view will be
* placed in.
*
* @param parent The parent that the created view will be placed
* in; <em>note that this may be null</em>.
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
AppCompatActivity中系統(tǒng)Factory實(shí)現(xiàn)
Activity常用的基類包括Activity,F(xiàn)ragmentActivity和AppCompatActivity堰汉,關(guān)于它們?nèi)叩膮^(qū)別辽社,可以參考我的文章Activity、FragmentActivity和AppCompatActivity的區(qū)別翘鸭。
其中AppCompatActivity在v7包中引入滴铅,查看其源碼,其中onCreate方法設(shè)置了一個(gè)AppCompatDelegate就乓。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
...
super.onCreate(savedInstanceState);
}
AppCompatDelegate是一個(gè)抽象基類汉匙,其對象實(shí)例根據(jù)手機(jī)sdk版本來初始化,具體可參考源碼生蚁。其中installViewFactory方法的實(shí)現(xiàn)如下噩翠。
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
上述代碼可知,如果AppCompatActivity未在onCreate之前設(shè)置LayoutInflater的Factory邦投,則AppCompatActivity會嘗試設(shè)置一個(gè)Factory2伤锚,其中Factory2在AppCompatDelegate的具體子類代碼中實(shí)現(xiàn)。注意志衣,在API Level 26及以后,LayoutInflaterCompat.setFactory被標(biāo)記為Deprecated屯援,故我參考的v27的源碼中使用的是LayoutInflaterCompat.setFactory2。
根據(jù)Activity念脯、FragmentActivity和AppCompatActivity的區(qū)別狸捅,官方提供的AppCompatDelegate子類實(shí)現(xiàn)薄榛,如AppCompatDelegateImplN。幫助我們實(shí)現(xiàn)了AppCompat風(fēng)格組件的向下兼容,利用AppCompatDelegateImplN提供的Factory2將TextView等組件替換為AppCompatTextView勉失,這樣就可以使用一些新的屬性,如autoSizeMinTextSize腺劣。
Activity中setFactory的兼容性問題
上面也提到過沛贪,通過setFactory或setFactory2可以實(shí)現(xiàn)一些特殊功能,如全局自定義View替換废登,應(yīng)用換膚等淹魄。但是需要注意兼容性問題,保證AppCompat風(fēng)格組件的正確替換堡距。
注意甲锡,需要在調(diào)用super.onCreate(savedInstanceState)之前進(jìn)行LayoutInflaterCompat.setFactory2的設(shè)置兆蕉。否則setFactory并不能進(jìn)行重復(fù)設(shè)置,會導(dǎo)致后設(shè)置的Factory失效缤沦。
探究AppCompatDelegateImplN中Factory2接口的具體實(shí)現(xiàn)虎韵。
/**
* From {@link LayoutInflater.Factory2}.
*/
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// First let the Activity's Factory try and inflate the view
final View view = callActivityOnCreateView(parent, name, context, attrs);
if (view != null) {
return view;
}
// If the Factory didn't handle it, let our createView() method try
return createView(parent, name, context, attrs);
}
可知最終是調(diào)用AppCompatDelegate實(shí)例中的createView方法進(jìn)行AppCompat組件的繪制。故兼容寫法如下:
public class MainActivity extends AppCompatActivity
{
private static final String TAG = "MainActivity";
if (typeface == null)
{
typeface = Typeface.createFromAsset(getAssets(), "x x.ttf");
}
@Override
protected void onCreate(Bundle savedInstanceState)
{
LayoutInflaterCompat.setFactory(LayoutInflater.from(this), new LayoutInflaterFactory()
{
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs)
{
//你可以在這里直接new自定義View
//你可以在這里將系統(tǒng)類替換為自定義View
//appcompat 創(chuàng)建view代碼
AppCompatDelegate delegate = getDelegate();
View view = delegate.createView(parent, name, context, attrs);
//替換字體示例
if ( view!= null && (view instanceof TextView))
{
((TextView) view).setTypeface(typeface);
}
return view;
}
});
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
Acitivity中setContentView的調(diào)用流程
以最常用的setContentView(@LayoutRes int layoutResID)方法為起點(diǎn)缸废,跟蹤view的繪制流程
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
關(guān)于Activity中的window實(shí)例創(chuàng)建包蓝,相信大家都有所了解。PhoneWindow是抽象基類window的具體實(shí)現(xiàn)企量,且該類內(nèi)部持有一個(gè)DecorView對象测萎,也即Activity界面的根View。
PhoneWindow的setContentView方法如下:
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
閱讀代碼届巩,可看到關(guān)鍵的調(diào)用語句mLayoutInflater.inflate(layoutResID, mContentParent)硅瞧,將資源文件構(gòu)建成View樹,并添加到mContentParent視圖中恕汇。其中mLayoutInflater是在PhoneWindow的構(gòu)造函數(shù)中得到實(shí)例對象的LayoutInflater.from(context)腕唧。可以多次調(diào)用setContentView()來顯示界面拇勃,每次繪制之前會調(diào)用removeAllViews來移除原有頁面四苇。
PhoneWindow類的setContentView(View view)方法和setContentView(View view, ViewGroup.LayoutParams params)方法源碼,原理同上方咆!
最終結(jié)合LayoutInflater的infalte方法月腋,參考Android LayoutInflater源碼解析,真正創(chuàng)建view的方法是LayoutInflater的createViewFromTag方法瓣赂。會依次調(diào)用mFactory2榆骚、mFactory和mPrivateFactory三者之一的onCreateView方法去創(chuàng)建一個(gè)View。如果不存在Factory煌集,則調(diào)用LayoutInflater自身的onCreateView或者createView來實(shí)例化View妓肢。
根據(jù)上面的流程可知,可通過setFactory或setFactory2來攔截view的創(chuàng)建過程苫纤,進(jìn)行一些特殊的操作碉钠。
Activity中onCreateView方法
Activity對象實(shí)現(xiàn)了LayoutInfalter.Factory2接口,提供了onCreateView方法的缺省實(shí)現(xiàn)卷拘。在Activity的attach方法中喊废,為Window的LayoutInflater設(shè)置了mPrivateFactory對象。也可以通過重新Activity的onCreateView方法進(jìn)行特定的操作栗弟。但是攔截時(shí)機(jī)晚于LayoutInfalter的setFactory和setFactory2方法污筷。
根據(jù)AppCompatActivity的學(xué)習(xí)也可知,在未對AppCompatActivity設(shè)置Factory或Factory2時(shí)乍赫,系統(tǒng)通過AppCompatDelegate自動(dòng)設(shè)置了Factory2實(shí)例瓣蛀。故一般的換膚方案都是通過setFactory或setFactory2實(shí)現(xiàn)對view創(chuàng)建過程的侵入陆蟆。
參考文章:
Android 探究 LayoutInflater setFactory
Android應(yīng)用setContentView與LayoutInflater加載解析機(jī)制源碼分析
https://github.com/hongyangAndroid/ChangeSkin
侵入性低擴(kuò)展性強(qiáng)的Android換膚框架XSkinLoader的用法及