Android Design Patterns (1) Continuation: Layout Inflater

Posted by nezbo on Mon, 15 Jul 2019 23:38:23 +0200

PhoneLayoutInflater

package android.view;
public abstract class LayoutInflater {
......
      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;
    }
......
}

Obviously this is an abstract class, so there must be a concrete implementation class.
As can be seen from the method of registration service in the previous article:

registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
                new CachedServiceFetcher<LayoutInflater>() {
            @Override
            public LayoutInflater createService(ContextImpl ctx) {
                return new PhoneLayoutInflater(ctx.getOuterContext());
            }});

The final real registration is the PhoneLayoutInflate class.
There is very little complete code for this class.

package com.android.internal.policy;
public class PhoneLayoutInflater extends LayoutInflater {
    private static final String[] sClassPrefixList = {
        "android.widget.",
        "android.webkit.",
        "android.app."
    };
    public PhoneLayoutInflater(Context context) {
        super(context);
    }
    protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {
        super(original, newContext);
    }
//The main thing is to rewrite this method.
    @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
        for (String prefix : sClassPrefixList) {
            try {
                View view = createView(name, prefix, attrs);
                if (view != null) {
                    return view;
                }
            } catch (ClassNotFoundException e) {
                // In this case we want to let the base class take a crack
                // at it.
            }
        }

        return super.onCreateView(name, attrs);
    }

    public LayoutInflater cloneInContext(Context newContext) {
        return new PhoneLayoutInflater(this, newContext);
    }
}

The onCreateView method calls the createView(String name, String prefix, AttributeSet attrs) method in the LayoutInflater class. It's mainly about "android.widget." "android.webkit.", The three parameters of "android.app." are passed in succession.
His function is to prefix the incoming view, for example, to change Button into android.widget.Button, and then create the corresponding View class based on the complete path.

This means that we do not need to write the complete package name in the layout file with the system control, but the complete package name must be written in the custom control and support package. Because the control of the system will fill in the full package name here.

So how does layout file xml relate to LayoutInflater?

Let's start from scratch.
The first step in using layout files is to call setContentView(R.layout.activity_main) in activity.
Take a look at Activity's source code:

package android.app;
public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2,
        Window.OnWindowDismissedCallback, WindowControllerCallback {
. . . 
        public void setContentView(@LayoutRes int layoutResID) {
                getWindow().setContentView(layoutResID);
                initWindowDecorActionBar();
        }
}

getWindow() gets the Windows class.

Let's first take a look at Activity's interface architecture.


UI Architecture Diagram, DecorView Wrong

Each Activity contains a Window, usually PhoneWindow. Phone Windows sets a DecorView to the root View of the entire window. DecorView is divided into two parts, one of which is the ContentView we set up. Phone Windows sets the layout to the ContentView of DecorView through setContentView.

Look at the previous code, getWindow().setContentView(layoutResID);.
getWindow() gets a Window, mWindow.

mWindow is initialized in the attach() method of Activity, which is the first method to execute after Activity is created.

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window) {
        attachBaseContext(context);
      ......
//Initialize Window s, which is PhoneWindow
        mWindow = new PhoneWindow(this, window);
        ......
        mWindow.getLayoutInflater().setPrivateFactory(this);
        ......
    }

Then look at PhoneWindow's setContentView method:

public class PhoneWindow extends Window implements MenuBuilder.Callback {
......
  public PhoneWindow(Context context) {
        super(context);
//Getting LayoutInflater by singleton in the construction method
        mLayoutInflater = LayoutInflater.from(context);
    }
......
      public void setContentView(int layoutResID) {
//mContentParent is the root layout DecorView of Phone Windows, which is created when DecorView is empty. Remove all Views instead of empty.
        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 {
//Layout Inflate is used here to load the layout.
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }
......
}

Enter LayoutInflater.inflate

package android.view;
public abstract class LayoutInflater {
      public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
        }
        ......
//The third parameter indicates whether to add to the parent View.
//PhoneWindow calls with DecorView, which must not be empty, so this value is TRUE.
      public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
        }
......
//The first parameter is the XML parser, which parses the layout file.
//The second parameter is the parent layout of the layout to parse.
//The third parameter is whether the parsed layout is added to the parent layout.
      public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;

            try {
                // Finding root Node
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }

                final String name = parser.getName();
            ......
                //merge tags are handled separately
                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }
    //Recursive Generation View
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Call createViewFromTag instead of merge tag to parse the layout directly
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        ......
                        // Generate layout parameters
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            //If you don't bind to the parent view, set the parameters to temp
                            temp.setLayoutParams(params);
                        }
                    }
......
//Parse all the sub-View s of the layout, which also calls the rInflate method.
                    rInflateChildren(parser, temp, attrs, true);
......
                    // If bound to the parent view, add temp to the parent view
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // Return to temp directly if you are not bound to the parent view
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
               ......
            }
            return result;
        }
    }


}

Recursion is used here, and depth-first traversal is used, so if the layout is too hierarchical, the efficiency will be very low.
Analyse the process:
(1) Parse the root element of the layout file
(2) If the root element is merge, rInflate is directly called to parse, and rInflate adds all the sub-views under the merge tag directly to the tag.
(3) If it is a common tag, call createViewFromTag to parse the element.
(4) Then call the rInflate method to recurse all the sub-elements of the root element and generate the View to load temp.
(5) Return to the parsed root view.

So parsing a single element is done using the following createViewFromTag method:

 View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }

        // Apply a theme wrapper, if allowed and one is specified.
        if (!ignoreThemeAttr) {
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            if (themeResId != 0) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();
        }

        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }

        try {
            View view;
//If you create LayoutInflate with another LayoutInflate, use the previous onCreateView method
            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);
            }

//New Layout Inflate
            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
//The name does not contain ".", similar to the < Button > description is a system control, according to
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
//Description is a custom control similar to <com.xxx.xxx.MyView>.
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }
            return view;
        } 
    }

Finally, call the createView method

public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class<? extends View> clazz = null;

        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);

            if (constructor == null) {
                // If the second parameter prefix is null, it means that name is the complete package name and is a custom control that directly reflects and loads the custom View.
//If the second parameter is not null, the full class name is composed of PhoneLayoutInflate plus a prefix.
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);

                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
//Store the constructor in the cache
                sConstructorMap.put(name, constructor);
            }
......
//Generate View
            final View view = constructor.newInstance(args);
            if (view instanceof ViewStub) {
//If it's ViewStub, it delays loading
                final ViewStub viewStub = (ViewStub) view;
                viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
            }
            return view;

        } catch (NoSuchMethodException e) {
        ......
        }
    }

summary

The process of LayoutInflater's work is:

  • Get the LayoutInflater service of the system first.
  • Then parse the incoming layout file xml, traverse each node with depth first, generate the corresponding View according to the name of each node and load it into the parent layout.
  • After the whole tree is loaded, a final View is formed and returned to complete the layout analysis.

Topics: Android xml Windows