PS: It has been proven that what you fear, you will avoid; you must believe in yourself.
LayoutInflater.Factory is a callback interface (Hook) provided for loading layouts, allowing you to customize layout files. Essentially, it enables you to modify a specific View based on the corresponding Tag in the callback of LayoutInflater.Factory and return it. The source code for LayoutInflater.Factory is as follows:
// LayoutInflater.java
public interface Factory {
/**
* @param name Tag name, such as TextView
* @param context Context environment
* @param attrs XML attributes
*
* @return View Newly created View; if null is returned, the Hook is invalid
*/
public View onCreateView(String name, Context context, AttributeSet attrs);
}
public interface Factory2 extends Factory {
// since API 11, an additional parameter parent has been added
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
It is known that the createViewFromTag method is called during the loading of layout files to create a View. It sequentially checks if mFactory, mFactory2, or mPrivateFactory is null. If the created View is not null, it directly returns that View. The source code is as follows:
// LayoutInflater.java
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) {
//...
View view;
// Sequentially check if mFactory, mFactory2, or mPrivateFactory is null; if the created View is not null, return it directly
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
// Parse View from the layout file
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
// ...
}
From the previous information, we know that LayoutInflater.Factory is used to modify layout files. This is certainly because somewhere mFactory or mFactory2 has been set. The source code shows that setFactory and setFactory2 are called in LayoutInflaterCompat. To achieve compatibility, LayoutInflaterCompat processes setFactory and setFactory2, where setFactory has been deprecated. The source code is as follows:
// LayoutInflaterCompat.java
@Deprecated
public static void setFactory(
@NonNull LayoutInflater inflater, @NonNull LayoutInflaterFactory factory) {
if (Build.VERSION.SDK_INT >= 21) {
inflater.setFactory2(factory != null ? new Factory2Wrapper(factory) : null);
} else {
final LayoutInflater.Factory2 factory2 = factory != null
? new Factory2Wrapper(factory) : null;
inflater.setFactory2(factory2);
final LayoutInflater.Factory f = inflater.getFactory();
if (f instanceof LayoutInflater.Factory2) {
// The merged factory is now set to getFactory(), but not getFactory2() (pre-v21).
// We will now try and force set the merged factory to mFactory2
forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
} else {
// Else, we will force set the original wrapped Factory2
forceSetFactory2(inflater, factory2);
}
}
}
public static void setFactory2(
@NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) {
inflater.setFactory2(factory);
if (Build.VERSION.SDK_INT < 21) {
final LayoutInflater.Factory f = inflater.getFactory();
if (f instanceof LayoutInflater.Factory2) {
// The merged factory is now set to getFactory(), but not getFactory2() (pre-v21).
// We will now try and force set the merged factory to mFactory2
forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
} else {
// Else, we will force set the original wrapped Factory2
forceSetFactory2(inflater, factory);
}
}
}
From the setFactory2 method, it uses the forceSetFactory2 method to forcibly set the mFactory2 attribute value of LayoutInflater through reflection. The source code is as follows:
// LayoutInflaterCompat.java
private static void forceSetFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {
if (!sCheckedField) {
try {
sLayoutInflaterFactory2Field = LayoutInflater.class.getDeclaredField("mFactory2");
sLayoutInflaterFactory2Field.setAccessible(true);
} catch (NoSuchFieldException e) {
Log.e(TAG, "forceSetFactory2 Could not find field 'mFactory2' on class "
+ LayoutInflater.class.getName()
+ "; inflation may have unexpected results.", e);
}
sCheckedField = true;
}
if (sLayoutInflaterFactory2Field != null) {
try {
sLayoutInflaterFactory2Field.set(inflater, factory);
} catch (IllegalAccessException e) {
Log.e(TAG, "forceSetFactory2 could not set the Factory2 on LayoutInflater "
+ inflater + "; inflation may have unexpected results.", e);
}
}
}
So where is the setFactory2 method of LayoutInflater called? The source code shows that it has been called in both AppCompatDelegateImpl and Fragment. Here, we will analyze it using AppCompatDelegateImpl as an example. The source code shows that there is a method installViewFactory in AppCompatDelegateImpl that uniformly sets the LayoutInflater.Factory. The source code is as follows:
// AppCompatDelegateImpl.java
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
// Uniformly set LayoutInflater.Factory
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
// If a LayoutInflater.Factory has been set, it loses support for new features and logs a message
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
Continuing to check, the installViewFactory method is called in the onCreate method of AppCompatActivity, as shown in the source code below:
// AppCompatActivity.java
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
// AppCompatActivity uniformly sets LayoutInflater.Factory
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
if (delegate.applyDayNight() && mThemeId != 0) {
if (Build.VERSION.SDK_INT >= 23) {
onApplyThemeResource(getTheme(), mThemeId, false);
} else {
setTheme(mThemeId);
}
}
super.onCreate(savedInstanceState);
}
Since Android 5.0, Google has added many new features, and to maintain backward compatibility, the support.v7 package was introduced, which includes the aforementioned AppCompatActivity. This is why in the createViewFromTag method, it checks mFactory1, mFactory2, etc. First, it tries to get the View from the default Factory's onCreateView method. If no LayoutInflater.Factory has been set, it directly parses the View from the layout file.
Since most created Activities inherit from AppCompatActivity, they default to setting the LayoutInflater.Factory. When using LayoutInflater to load layout files, it will call its onCreateView method. The source code shows that both AppCompatDelegateImpl and Activity implement the LayoutInflater.Factory2 interface. Here, we will look at the specific implementation in AppCompatDelegateImpl. The source code is as follows:
/**
* From {@link LayoutInflater.Factory2}.
*/
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
return createView(parent, name, context, attrs);
}
/**
* From {@link LayoutInflater.Factory2}.
*/
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return onCreateView(null, name, context, attrs);
}
It can be seen that the final call is to the createView method in AppCompatDelegateImpl, as shown in the source code below:
// AppCompatDelegateImpl.java
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
if (mAppCompatViewInflater == null) {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
String viewInflaterClassName =
a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
if ((viewInflaterClassName == null)
|| AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
// Either default class name or set explicitly to null. In both cases
// create the base inflater (no reflection)
mAppCompatViewInflater = new AppCompatViewInflater();
} else {
try {
Class viewInflaterClass = Class.forName(viewInflaterClassName);
mAppCompatViewInflater =
(AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
.newInstance();
} catch (Throwable t) {
Log.i(TAG, "Failed to instantiate custom view inflater "
+ viewInflaterClassName + ". Falling back to default.", t);
mAppCompatViewInflater = new AppCompatViewInflater();
}
}
}
boolean inheritContext = false;
if (IS_PRE_LOLLIPOP) {
inheritContext = (attrs instanceof XmlPullParser)
// If we have a XmlPullParser, we can detect where we are in the layout
? ((XmlPullParser) attrs).getDepth() > 1
// Otherwise we have to use the old heuristic
: shouldInheritContext((ViewParent) parent);
}
// Key position
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
true, /* Read read app:theme as a fallback at all times for legacy reasons */
VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
);
}
In the above code, it first obtains the defined attribute set, then checks if there is a custom Inflater. If a custom one is defined, it reflects the creation of the AppCompatViewInflater object based on the full class name; otherwise, it creates the default AppCompatViewInflater. Finally, it calls the corresponding Inflater's createView method.
At this point, it is clear that the AppCompatViewInflater can be used directly, but why is it so complicated? This indicates a strong extensibility, allowing for a custom Inflater to replace the official AppCompatViewInflater. Continuing to check the createView method of AppCompatViewInflater, the source code is as follows:
// AppCompatViewInflater.java
final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
final Context originalContext = context;
// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
// by using the parent's context
if (inheritContext && parent != null) {
context = parent.getContext();
}
if (readAndroidTheme || readAppTheme) {
context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
}
if (wrapContext) {
context = TintContextWrapper.wrap(context);
}
View view = null;
// We need to 'inject' our tint aware Views in place of the standard framework versions
switch (name) {
case "TextView":
view = createTextView(context, attrs);
verifyNotNull(view, name);
break;
case "ImageView":
view = createImageView(context, attrs);
verifyNotNull(view, name);
break;
case "Button":
view = createButton(context, attrs);
verifyNotNull(view, name);
break;
case "EditText":
view = createEditText(context, attrs);
verifyNotNull(view, name);
break;
case "Spinner":
view = createSpinner(context, attrs);
verifyNotNull(view, name);
break;
case "ImageButton":
view = createImageButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckBox":
view = createCheckBox(context, attrs);
verifyNotNull(view, name);
break;
case "RadioButton":
view = createRadioButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckedTextView":
view = createCheckedTextView(context, attrs);
verifyNotNull(view, name);
break;
case "AutoCompleteTextView":
view = createAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "MultiAutoCompleteTextView":
view = createMultiAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "RatingBar":
view = createRatingBar(context, attrs);
verifyNotNull(view, name);
break;
case "SeekBar":
view = createSeekBar(context, attrs);
verifyNotNull(view, name);
break;
default:
view = createView(context, name, attrs);
}
if (view == null && originalContext != context) {
// If the View cannot be created based on name, use reflection to create it based on name
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
checkOnClickListener(view, attrs);
}
return view;
}
The above code replaces the app's components with their corresponding compatible versions based on the component's name, such as replacing TextView with AppCompatTextView. If it cannot create a View based on the name, it calls createViewFromTag to create the View. The source code is as follows:
// AppCompatViewInflater.java
private View createViewFromTag(Context context, String name, AttributeSet attrs) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
try {
mConstructorArgs[0] = context;
mConstructorArgs[1] = attrs;
// If there is no dot in name, try adding the system component prefix
if (-1 == name.indexOf('.')) {
for (int i = 0; i < sClassPrefixList.length; i++) {
final View view = createViewByPrefix(context, name, sClassPrefixList[i]);
if (view != null) {
return view;
}
}
return null;
} else { // Do not add prefix
return createViewByPrefix(context, name, null);
}
} catch (Exception e) {
return null;
} finally {
// Don't retain references on context.
mConstructorArgs[0] = null;
mConstructorArgs[1] = null;
}
}
It can be seen that createViewFromTag attempts to add the system component prefix to the name, and ultimately calls createViewByPrefix to create the View. The createViewByPrefix method uses reflection to create the object, as shown in the source code below:
// AppCompatViewInflater.java
private View createViewByPrefix(Context context, String name, String prefix)
throws ClassNotFoundException, InflateException {
// Utilizing caching
Constructor<? extends View> constructor = sConstructorMap.get(name);
try {
if (constructor == null) {
Class<? extends View> clazz = context.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
constructor = clazz.getConstructor(sConstructorSignature);
sConstructorMap.put(name, constructor);
}
constructor.setAccessible(true);
return constructor.newInstance(mConstructorArgs);
} catch (Exception e) {
return null;
}
}
In addition to using reflection to create the View, a HashMap is used to cache the View's constructor methods, where it first retrieves the corresponding constructor method from the cache. If the cache does not contain the corresponding constructor method instance, it will reflectively create the method instance, ultimately completing the View creation through reflection. Thus, if a LayoutInflater.Factory is set, the process of creating the View is essentially as described above.
From the analysis above, we know that when loading layout files, it first checks if a LayoutInflater.Factory has been set. If it has, the specific rules of the LayoutInflater.Factory will be used to create the View; otherwise, it will directly use reflection to create the View. Therefore, a custom LayoutInflater.Factory can be defined to create Views according to one's own rules, as shown below:
// First method
@Override
protected void onCreate(Bundle savedInstanceState) {
LayoutInflater layoutInflater = getLayoutInflater();
// Set LayoutInflater.Factory; must be set before super.onCreate
layoutInflater.setFactory(new LayoutInflater.Factory() {
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
// Replace TextView with Button and return
if (name.equals("TextView")){
Button button = new Button(MainActivity.this);
button.setText("I have been replaced with a Button");
return button;
}
return null;
}
});
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
With the above code, when parsing Views while loading layout files, all TextViews will be replaced with Buttons. Other Views will not automatically convert to AppCompatXxx series Views, which means that when a custom LayoutInflater.Factory is set, other Views will lose support for new features, as indicated by the corresponding log content:
I/AppCompatDelegate: The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's
So how can we ensure that we can continue to use new features while also replacing Views? From the previous analysis, we know that the replacement of system components is called in the createView method of AppCompatDelegateImpl, and this method is public. Therefore, as long as we can continue to call the createView method of AppCompatDelegateImpl after customization, we can ensure that other Views are not affected, as shown below:
// First method
LayoutInflaterCompat.setFactory2(layoutInflater, new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (name.equals("TextView")){
Button button = new Button(MainActivity.this);
button.setText("I have been replaced with a Button");
return button;
}
AppCompatDelegate compatDelegate = getDelegate();
View view = compatDelegate.createView(parent,name,context,attrs);
return view;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return onCreateView(null,name,context,attrs);
}
});
Below are the layout view structures of the default layout and the two different replacement methods as shown in the following diagram:
Additionally, LayoutInflater.Factory can be used for global font replacement, skin changing, and other functionalities. This concludes the source code analysis of LayoutInflater.Factory.