LayoutInflater.setFactory学习及进阶
相信大家对LayoutInflater都不陌生,它经常被用来根据xml生成View。比较熟悉的方法包括:
- LayoutInflater.from(Context context)
- inflate(@LayoutRes int resource, @Nullable ViewGroup root)
- inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
构造方法源码如下,可见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;
}
除了上述方法,我今天想介绍的是相对不常用的两个方法。
- setFactory(Factory factory)
- setFactory2(Factory2 factory)
这两个方法基本功能一致。系统通过Factory提供了一种hook的方法,方便开发者拦截LayoutInflater创建View的过程。应用场景包括1)在XML布局中自定义标签名称;2)全局替换系统控件为自定义View; 3)替换app中字体;4)全局换肤等。
Factory与Factory2的区别
二者都是LayoutInflater类内部定义的接口。Factory2继承自Factory接口,在API 11(HONEYCOMB)中引入的。Factory2比Factory多增加了一个onCreateView(View parent, String name, Context context, AttributeSet attrs),该方法多了一个parent,用来存放构建出的View。
Android在v4包中提供了LayoutInflaterCompat来帮助完成兼容性的操作。
-
setFactory(
@NonNull LayoutInflater inflater, @NonNull LayoutInflaterFactory factory)
在API Level 26.1.0中被标记为Deprecated,官方推荐使用setFactory2方法 - setFactory2(
@NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory)
Factory接口的定义如下,该接口只有一个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中系统Factory实现
Activity常用的基类包括Activity,FragmentActivity和AppCompatActivity,关于它们三者的区别,可以参考我的文章Activity、FragmentActivity和AppCompatActivity的区别。
其中AppCompatActivity在v7包中引入,查看其源码,其中onCreate方法设置了一个AppCompatDelegate。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
...
super.onCreate(savedInstanceState);
}
AppCompatDelegate是一个抽象基类,其对象实例根据手机sdk版本来初始化,具体可参考源码。其中installViewFactory方法的实现如下。
@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之前设置LayoutInflater的Factory,则AppCompatActivity会尝试设置一个Factory2,其中Factory2在AppCompatDelegate的具体子类代码中实现。注意,在API Level 26及以后,LayoutInflaterCompat.setFactory被标记为Deprecated,故我参考的v27的源码中使用的是LayoutInflaterCompat.setFactory2。
根据Activity、FragmentActivity和AppCompatActivity的区别,官方提供的AppCompatDelegate子类实现,如AppCompatDelegateImplN。帮助我们实现了AppCompat风格组件的向下兼容,利用AppCompatDelegateImplN提供的Factory2将TextView等组件替换为AppCompatTextView,这样就可以使用一些新的属性,如autoSizeMinTextSize。
Activity中setFactory的兼容性问题
上面也提到过,通过setFactory或setFactory2可以实现一些特殊功能,如全局自定义View替换,应用换肤等。但是需要注意兼容性问题,保证AppCompat风格组件的正确替换。
注意,需要在调用super.onCreate(savedInstanceState)之前进行LayoutInflaterCompat.setFactory2的设置。否则setFactory并不能进行重复设置,会导致后设置的Factory失效。
探究AppCompatDelegateImplN中Factory2接口的具体实现。
/**
* 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);
}
可知最终是调用AppCompatDelegate实例中的createView方法进行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
//你可以在这里将系统类替换为自定义View
//appcompat 创建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的调用流程
以最常用的setContentView(@LayoutRes int layoutResID)方法为起点,跟踪view的绘制流程
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
关于Activity中的window实例创建,相信大家都有所了解。PhoneWindow是抽象基类window的具体实现,且该类内部持有一个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;
}
阅读代码,可看到关键的调用语句mLayoutInflater.inflate(layoutResID, mContentParent),将资源文件构建成View树,并添加到mContentParent视图中。其中mLayoutInflater是在PhoneWindow的构造函数中得到实例对象的LayoutInflater.from(context)。可以多次调用setContentView()来显示界面,每次绘制之前会调用removeAllViews来移除原有页面。
PhoneWindow类的setContentView(View view)方法和setContentView(View view, ViewGroup.LayoutParams params)方法源码,原理同上!
最终结合LayoutInflater的infalte方法,参考Android LayoutInflater源码解析,真正创建view的方法是LayoutInflater的createViewFromTag方法。会依次调用mFactory2、mFactory和mPrivateFactory三者之一的onCreateView方法去创建一个View。如果不存在Factory,则调用LayoutInflater自身的onCreateView或者createView来实例化View。
根据上面的流程可知,可通过setFactory或setFactory2来拦截view的创建过程,进行一些特殊的操作。
Activity中onCreateView方法
Activity对象实现了LayoutInfalter.Factory2接口,提供了onCreateView方法的缺省实现。在Activity的attach方法中,为Window的LayoutInflater设置了mPrivateFactory对象。也可以通过重新Activity的onCreateView方法进行特定的操作。但是拦截时机晚于LayoutInfalter的setFactory和setFactory2方法。
根据AppCompatActivity的学习也可知,在未对AppCompatActivity设置Factory或Factory2时,系统通过AppCompatDelegate自动设置了Factory2实例。故一般的换肤方案都是通过setFactory或setFactory2实现对view创建过程的侵入。
参考文章:
Android 探究 LayoutInflater setFactory
Android应用setContentView与LayoutInflater加载解析机制源码分析
https://github.com/hongyangAndroid/ChangeSkin
侵入性低扩展性强的Android换肤框架XSkinLoader的用法及