核心知識
-
你可以在壓榨布局的時候通過LayoutInflater.Factory針對view的創(chuàng)建進行hook操作
(比如實現(xiàn)動態(tài)換膚)
-
LayoutInflater.setFactory 不能在 super.onCreate 之后使用抵乓。
(因為在onCreate時系統(tǒng)會設(shè)置一個factory痕支,如果重復(fù)設(shè)置factory系統(tǒng)將會拋出異常,不過我們可以反射修改LayoutInflater的mFactorySet屬性來避免拋出異常)
-
AppCompatActivity 為什么 setFactory 净赴?向下兼容新版本中的效果阎肝。
( AppCompatActivity 設(shè)置 Factory 是為了將一些 widget 自動變成 兼容widget 瞻讽,例如將 TextView 變成 AppCompatTextView巍举,以便于向下兼容新版本中的效果溶其,在高版本中的一些 widget 新特性就是這樣在老版本中也能展示的。)
- LayoutInflater.Factory2 繼承自 LayoutInflater.Factory
createViewFromTag()中的factory
在View創(chuàng)建時有一個createViewFromTag()方法策橘,在這個方法開頭有這么一段源碼
View view;
if (mFactory2 != null) {
// ① 有mFactory2炸渡,則調(diào)用mFactory2的onCreateView方法
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
// ② 有mFactory,則調(diào)用mFactory的onCreateView方法
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
//后面的源碼意思是如果沒有factory的實例丽已,就用系統(tǒng)的方式創(chuàng)建view蚌堵。
這段代碼的意思是,如果factory2不為空沛婴,則用factory2的實例創(chuàng)建view吼畏,如果mFactory不為空,則用mFactory的實例創(chuàng)建view嘁灯。 也就是說泻蚊,這兩個方法是用來讓我們覆蓋view創(chuàng)建的入口。
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)
那么我們就明白了,如果我們設(shè)置了LayoutInflater Factory 羹奉,在LayoutInflater 的 createViewFromTag 方法中就會通過這個 Factory 的 onCreateView 方法來創(chuàng)建 View秒旋。
Factory 作用
那我們可以進行什么hook操作呢? 舉個簡單的例子:比如你在 XML中 寫了一個 TextView標簽诀拭,然后在 onCreateView 這個回調(diào)里 判斷如果 name 是 TextView 的話可以變成一個Button迁筛,這樣的功能可以實現(xiàn)例如批量更換某一個控件等的用途。例子如下:
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.liuzhaofutrue.teststart.MainActivity">
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
接下來我們在 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瑰煎,但是現(xiàn)在卻被改造成了一個 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 已經(jīng)被標記為過時,更建議使用 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);
}
}
這里調(diào)用 setFactory 實際上還是調(diào)用的 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 已經(jīng)被設(shè)置了一個 Factory农尖,而我們再設(shè)置的時候就會報錯析恋。我們跟蹤下 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 方法只能被調(diào)用一次,重復(fù)設(shè)置則會拋出異常盛卡。那Factory2是被誰設(shè)置了呢助隧? 我們來看下 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);
}
其中會調(diào)用 delegate.installViewFactory(); 最終會調(diào)用到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 會自動設(shè)置一個 Factory2并村,難怪我們在 super.onCreate 之后調(diào)用會報錯;
所以我們明白了滓技,為什么我們在 super.onCreate 之前設(shè)置 Factory之后哩牍,系統(tǒng)再次設(shè)置 Factory 的時候不會拋出異常
AppCompatActivity 為什么 setFactory
那么為什么 AppCompatActivity 會自動設(shè)置一個 Factory呢?順著 AppCompatDelegateImplV9 的 installViewFactory方法繼續(xù)跟蹤令漂,走到了 onCreateView 方法膝昆,它最終會調(diào)用到 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 設(shè)置 Factory 是為了將一些 widget 自動變成 兼容widget (例如將 TextView 變成 AppCompatTextView)以便于向下兼容新版本中的效果叠必,在高版本中的一些 widget 新特性就是這樣在老版本中也能展示的荚孵。
那如果我們設(shè)置了自己的 Factory 豈不是就避開了系統(tǒng)的兼容?其實系統(tǒng)的兼容我們?nèi)匀豢梢员4嫦聛砟铀簦驗橄到y(tǒng)是通過 AppCompatDelegate.onCreateView 方法來實現(xiàn) widget 兼容的处窥,那我們就可以在設(shè)置 Factory 的時候先調(diào)用 AppCompatDelegate.onCreateView 方法嘱吗,再來做我們的處理玄组。
LayoutInflater.from(this).setFactory2(new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// 調(diào)用 AppCompatDelegate 的createView方法
getDelegate().createView(parent, name, context, attrs);
// 再來執(zhí)行我們的定制化操作
return null;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});