核心知识
-
你可以在压榨布局的时候通过LayoutInflater.Factory针对view的创建进行hook操作
(比如实现动态换肤)
-
LayoutInflater.setFactory 不能在 super.onCreate 之后使用。
(因为在onCreate时系统会设置一个factory,如果重复设置factory系统将会抛出异常,不过我们可以反射修改LayoutInflater的mFactorySet属性来避免抛出异常)
-
AppCompatActivity 为什么 setFactory ?向下兼容新版本中的效果。
( AppCompatActivity 设置 Factory 是为了将一些 widget 自动变成 兼容widget ,例如将 TextView 变成 AppCompatTextView,以便于向下兼容新版本中的效果,在高版本中的一些 widget 新特性就是这样在老版本中也能展示的。)
- LayoutInflater.Factory2 继承自 LayoutInflater.Factory
createViewFromTag()中的factory
在View创建时有一个createViewFromTag()方法,在这个方法开头有这么一段源码
View view;
if (mFactory2 != null) {
// ① 有mFactory2,则调用mFactory2的onCreateView方法
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
// ② 有mFactory,则调用mFactory的onCreateView方法
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
//后面的源码意思是如果没有factory的实例,就用系统的方式创建view。
这段代码的意思是,如果factory2不为空,则用factory2的实例创建view,如果mFactory不为空,则用mFactory的实例创建view。 也就是说,这两个方法是用来让我们覆盖view创建的入口。
LayoutInflater.Factory
LayoutInflater.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.
你可以在压榨布局的时候通过LayoutInflater.Factory进行hook操作 ,你可以使用LayoutInflater.Factory 去自定义xml布局文件中的tag(标签)名称
我们来看下这个唯一的方法:
public abstract View onCreateView (String name, Context context, AttributeSet attrs)
那么我们就明白了,如果我们设置了LayoutInflater Factory ,在LayoutInflater 的 createViewFromTag 方法中就会通过这个 Factory 的 onCreateView 方法来创建 View。
Factory 作用
那我们可以进行什么hook操作呢? 举个简单的例子:比如你在 XML中 写了一个 TextView标签,然后在 onCreateView 这个回调里 判断如果 name 是 TextView 的话可以变成一个Button,这样的功能可以实现例如批量更换某一个控件等的用途。例子如下:
接下来我们在 Java 代码中做修改:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
LayoutInflater.from(this).setFactory2(new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if(TextUtils.equals(name,"TextView")){
Button button = new Button(MainActivity.this);
button.setText("我替换了TextView");
button.setAllCaps(false);
return button;
}
return getDelegate().createView(parent, name, context, attrs);
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
可以看到,本来在布局文件中需要展示的是一个 TextView,但是现在却被改造成了一个 Button。
LayoutInflaterCompat
LayoutInflater.Factory2 是API 11 被加进来的,那么 LayoutInflaterCompat 就是拿来做兼容的类。我们来看下它最重要的两个方法:
@Deprecated
public static void setFactory(
@NonNull LayoutInflater inflater, @NonNull LayoutInflaterFactory factory) {
IMPL.setFactory(inflater, factory);
}
public static void setFactory2(
@NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) {
IMPL.setFactory2(inflater, factory);
}
可以看到 setFactory 已经被标记为过时,更建议使用 setFactory2 方法。
static final LayoutInflaterCompatBaseImpl IMPL;
static {
if (Build.VERSION.SDK_INT >= 21) {
IMPL = new LayoutInflaterCompatApi21Impl();
} else {
IMPL = new LayoutInflaterCompatBaseImpl();
}
}
@RequiresApi(21)
static class LayoutInflaterCompatApi21Impl extends LayoutInflaterCompatBaseImpl {
@SuppressWarnings("deprecation")
@Override
public void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
inflater.setFactory2(factory != null ? new Factory2Wrapper(factory) : null);
}
@Override
public void setFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {
inflater.setFactory2(factory);
}
}
这里调用 setFactory 实际上还是调用的 setFactory2 方法。
LayoutInflater.setFactory 使用注意
如果我们将LayoutInflater.setFactory 挪到 super.onCreate 的后面可以吗? 程序竟然报错了,我们看下Log:
Process: com.example.teststart, PID: 24132
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.teststart/com.example.teststart.MainActivity}: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2876)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2941)
Caused by: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater
at android.view.LayoutInflater.setFactory2(LayoutInflater.java:317)
at com.example.teststart.MainActivity.onCreate(MainActivity.java:18)
at android.app.Activity.performCreate(Activity.java:6765)
说明是 LayoutInflater 已经被设置了一个 Factory,而我们再设置的时候就会报错。我们跟踪下 LayoutInflater.from(this).setFactory2 方法:
private boolean mFactorySet;
public void setFactory2(Factory2 factory) {
if (mFactorySet) {
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
if (factory == null) {
throw new NullPointerException("Given factory can not be null");
}
mFactorySet = true;
if (mFactory == null) {
mFactory = mFactory2 = factory;
} else {
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
可以通过这个 mFactorySet 变量看出 setFactory2 方法只能被调用一次,重复设置则会抛出异常。那Factory2是被谁设置了呢? 我们来看下 AppCompatActivity 的 onCreate 方法
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
if (delegate.applyDayNight() && mThemeId != 0) {
// If DayNight has been applied, we need to re-apply the theme for
// the changes to take effect. On API 23+, we should bypass
// setTheme(), which will no-op if the theme ID is identical to the
// current theme ID.
if (Build.VERSION.SDK_INT >= 23) {
onApplyThemeResource(getTheme(), mThemeId, false);
} else {
setTheme(mThemeId);
}
}
super.onCreate(savedInstanceState);
}
其中会调用 delegate.installViewFactory(); 最终会调用到AppCompatDelegateImplV9 的 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");
}
}
}
可以看到:
如果 layoutInflater.getFactory() 为空,则 AppCompatActivity 会自动设置一个 Factory2,难怪我们在 super.onCreate 之后调用会报错;
所以我们明白了,为什么我们在 super.onCreate 之前设置 Factory之后,系统再次设置 Factory 的时候不会抛出异常
AppCompatActivity 为什么 setFactory
那么为什么 AppCompatActivity 会自动设置一个 Factory呢?顺着 AppCompatDelegateImplV9 的 installViewFactory方法继续跟踪,走到了 onCreateView 方法,它最终会调用到 AppCompatViewInflater 的 createView 方法。
public final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
View view = null;
// We need to 'inject' our tint aware Views in place of the standard framework versions
switch (name) {
case "TextView":
view = new AppCompatTextView(context, attrs);
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
case "Button":
view = new AppCompatButton(context, attrs);
break;
......
}
return view;
}
原来 AppCompatActivity 设置 Factory 是为了将一些 widget 自动变成 兼容widget (例如将 TextView 变成 AppCompatTextView)以便于向下兼容新版本中的效果,在高版本中的一些 widget 新特性就是这样在老版本中也能展示的。
那如果我们设置了自己的 Factory 岂不是就避开了系统的兼容?其实系统的兼容我们仍然可以保存下来,因为系统是通过 AppCompatDelegate.onCreateView 方法来实现 widget 兼容的,那我们就可以在设置 Factory 的时候先调用 AppCompatDelegate.onCreateView 方法,再来做我们的处理。
LayoutInflater.from(this).setFactory2(new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// 调用 AppCompatDelegate 的createView方法
getDelegate().createView(parent, name, context, attrs);
// 再来执行我们的定制化操作
return null;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});