banner
jzman

jzman

Coding、思考、自觉。
github

Usage and Source Code Analysis of LayoutInflater.Factory

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, which allows you to customize layout files. In fact, you can modify a specific View based on the corresponding Tag in the callback of LayoutInflater.Factory and then return it. The source code of 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 when loading the layout file to create a View. It will sequentially check if mFactory, mFactory2, and mPrivateFactory are null. If the created View is not null, it will directly return 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, and mPrivateFactory are 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 means that somewhere mFactory or mFactory2 has already been set. Looking at the source code, we find that setFactory and setFactory2 are called in LayoutInflaterCompat. LayoutInflaterCompat processes setFactory and setFactory2 for compatibility purposes, 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, we see that 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? Looking at the source code, it is 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 is set by itself, 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 look, 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, checks for mFactory1, mFactory2, etc., are performed. It first 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 automatically set the LayoutInflater.Factory. When using LayoutInflater to load layout files, it will call its onCreateView method. Looking at the source code, both AppCompatDelegateImpl and Activity implement the LayoutInflater.Factory2 interface. Here we look at the specific implementation in AppCompatDelegateImpl, as shown in the source code below:

/**
 * 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 creates an AppCompatViewInflater object via reflection 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 go through such a complicated process? This indicates a strong extensibility, allowing for the customization of the Inflater to replace the official AppCompatViewInflater. Continuing to look at 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, create it using reflection 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, as shown in the source code below:

// 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 the 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 the createViewFromTag method attempts to add system component prefixes 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 cached usage
    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, it also uses a HashMap to cache the constructor methods to reduce performance consumption. It first retrieves the corresponding constructor method from the cache; if it does not exist, it reflects to create the constructor method instance, ultimately completing the View creation via reflection. Thus, if a LayoutInflater.Factory is set, the process of creating the View is basically 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, you can customize the LayoutInflater.Factory to create Views according to your 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 the View while loading the layout file, all TextViews will be replaced with Buttons, while other Views will automatically convert to AppCompatXxx series Views. This means that when you set the LayoutInflater.Factory yourself, other Views will lose support for new features, corresponding to the log content as follows:

I/AppCompatDelegate: The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's

So how can you ensure that you can continue to use new features while also replacing Views? From the previous analysis, we know that the replacement of system components calls the createView method of AppCompatDelegateImpl, and this method is public. Therefore, as long as you can continue to call the createView method of AppCompatDelegateImpl after customizing, you 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, you can use LayoutInflater.Factory to globally replace fonts, skin, and other functions for extension. The source code analysis of LayoutInflater.Factory ends here.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.