Look at LayoutInflater again, this time you may have a new understanding

This article is simultaneously published on my WeChat official account. You can follow by scanning the QR code at the bottom of the article or searching for Guo Lin on WeChat. Articles are updated every working day.

Today I don’t want to talk about some new functions and features of Android. I especially want to talk about this old-fashioned topic: LayoutInflater.

Most of the people who read my articles now are some old Androids. I believe that it is common for everyone to use LayoutInflater, and it is easy to use.

But even so, I still think this knowledge point can be analyzed. After reading it, you may have some new understanding of LayoutInflater.

First summarize what LayoutInflater is used for.

We all know that when developing Android applications, the layout is basically written through xml files. Of course, you can also write the layout purely by hand in the code, but anyone who has written it knows that writing the layout in this way will be very troublesome.

So how is the layout file written by xml converted into a View object in Android and displayed in the application? This is the role of LayoutInflater.

Simply put, the job of LayoutInflater is to convert the layout written in xml files into View objects in Android, and this is the only way to convert xml layouts into Views in Android.

Maybe some friends will say, no, I haven’t used LayoutInflater very much. Isn’t it enough to call the setContentView() method in the Activity to convert the xml layout into a View?

This is because the Android SDK has done some good encapsulation for us on the upper layer, which makes the development work easier. If you open the source code of the setContentView() method to find out, you will find that its bottom layer also uses LayoutInflater:

@Override
public void setContentView(int resId) {
    
    
    ensureSubDecor();
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mAppCompatWindowCallback.getWrapped().onContentChanged();
}

So how does LayoutInflater convert an xml layout into a View object?

This is of course a very complicated process, but if you briefly summarize it, the most important steps are nothing more than two steps:

  1. The content in the xml file is parsed out through the parser.
  2. Use reflection to create a View object from the parsed elements.

Here I don't want to lead you to follow the source code step by step in the article, so the article may look tired and boring, so I only post some codes that I think are more critical.

Code snippet for parsing the content of the xml file:

public View inflate(@LayoutRes int resource, 
                    @Nullable ViewGroup root, 
                    boolean attachToRoot) {
    
    
    ...
    XmlResourceParser parser = res.getLayout(resource);
    try {
    
    
        return inflate(parser, root, attachToRoot);
    } finally {
    
    
        parser.close();
    }
}

It can be seen that an XmlResourceParser object is obtained here, which is used to parse the xml file. Since the specific parsing rules are too complicated, we will not follow up.

Code snippet for creating a View object using reflection:

public final View createView(@NonNull Context viewContext, @NonNull String name,
            @Nullable String prefix, @Nullable AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
    
    
    ...
    if (constructor == null) {
    
    
        // Class not found in the cache, see if it's real, and try to add it
        clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                mContext.getClassLoader()).asSubclass(View.class);
        constructor = clazz.getConstructor(mConstructorSignature);
        constructor.setAccessible(true);
        sConstructorMap.put(name, constructor);
    }
    ...
    try {
    
    
        final View view = constructor.newInstance(args);
        if (view instanceof ViewStub) {
    
    
            // Use the same context when inflating ViewStub later.
            final ViewStub viewStub = (ViewStub) view;
            viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
        }
        return view;
    }
    ...
}

Seeing this, we have a basic understanding of the general working principle of LayoutInflater.

But as mentioned earlier, this article is not to take you to read the source code, but to have a new understanding of LayoutInflater from the usage level.

Then the most common usage of LayoutInflater is as follows:

View view = LayoutInflater.from(context).inflate(resourceId, parent, false);

The meaning of this code is to first call the from() method of LayoutInflater to obtain an instance of LayoutInflater, and then call its inflate() method to parse and load a layout, convert it into a View object and return it.

However, I think this code is extremely unfriendly for novices, and even for many veterans.

Let's take a look at the parameter definition of the inflate() method:

public View inflate(int resource, 
                    @Nullable ViewGroup root, 
                    boolean attachToRoot) {
    
    
    ...
}

The inflate() method receives 3 parameters. The first parameter resource is relatively easy to understand, which is the resource id of the xml file we want to parse and load. What does the second parameter root, and the third parameter attachToRoot mean? Maybe even many programmers who have done Android development for many years may not be able to explain clearly.

And this code will definitely be used when we use RecyclerView or Fragment. When I was writing "The First Line of Code", I had to talk about the usage of RecyclerView in a very early chapter, but I found it difficult to explain the content of LayoutInflater to beginners, so I always felt that this content was not explained well. . You can only use rote memorization first, and temporarily remember that this part of the code must be written in this way.

Today, I hope to really explain LayoutInflater clearly.

We know that Android's layout structure is a tree structure. Each layout can contain several sub-layouts, and each sub-layout can continue to contain sub-layouts, so as to construct a View of any style and present it to the user.

Therefore, we can roughly understand that each layout must have a parent layout.

This is also the role of the second parameter root of the inflate() method, which is to specify a parent layout for the current XML layout to be parsed and loaded.

So can a layout have no parent layout? Of course it is also possible, which is why the root parameter is marked as @Nullable.

But if we inflate a layout without a parent layout, how to display it? Naturally, there is no way to display it, so it can only be added to an existing layout later by using addView. Or the layout you inflate out is a top-level layout, so it doesn't need to have a parent layout. But these scenarios are relatively rare, so in most cases, we need to specify the parent layout when using the inflate() method of LayoutInflater.

In addition, if you do not specify a parent layout for the inflate layout, another problem will arise. Let's explain it through an example.

Here we define a button_layout.xml layout file, the code is as follows:

<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button" />

This layout file is very simple, with only one button in it.

Next we use LayoutInflater to load this layout file and add it to an existing layout:

public class MainActivity extends Activity {
    
    
 
	@Override
	protected void onCreate(Bundle savedInstanceState) {
    
    
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		LinearLayout mainLayout = (LinearLayout) findViewById(R.id.main_layout);
		View buttonLayout = LayoutInflater.from(this).inflate(R.layout.button_layout, null);
		mainLayout.addView(buttonLayout);
	}
 
}

As you can see, here we did not specify the parent layout for button_layout, but passed in a null. When the second parameter is null, the third parameter is meaningless, so you don't need to specify it.

But as I said before, a layout cannot be displayed without a parent layout, so we used the addView() method to add it to an existing layout.

The code is so simple, now we can run the program, the effect is shown in the following figure:

It seems that there is no problem, and the button can be displayed normally, indicating that the button_layout.xml layout has indeed been successfully loaded and added to the existing layout.

But if you try to adjust the size of the button, you will find that no matter how you adjust it, the size of the button will not change:

<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="300dp"
    android:layout_height="100dp"
    android:text="Button" />

Here we specify the width and height of the button as 300dp, and the height as 100dp, and re-run the program interface without any change.

Why is there such a situation?

In fact, no matter how much you modify the layout_width and layout_height values ​​of the Button, it will have no effect, because these two values ​​​​have completely lost their effect now. Usually we often use layout_width and layout_height to set the size of the View, and it always works normally, as if these two properties are indeed used to set the size of the View.

In fact, it is not the case. They are actually used to set the size of the View in the layout. That is to say, the View must exist in a layout first. That's why these two properties are called layout_width and layout_height instead of width and height.

And because we did not specify a parent layout for the button_layout.xml layout when loading it with LayoutInflater, the layout_width and layout_height attributes are useless here. More precisely, all attributes starting with layout_ will lose their effect.

Now we modify the code as follows:

public class MainActivity extends Activity {
    
    
 
	@Override
	protected void onCreate(Bundle savedInstanceState) {
    
    
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		LinearLayout mainLayout = (LinearLayout) findViewById(R.id.main_layout);
		View buttonLayout = LayoutInflater.from(this).inflate(R.layout.button_layout, mainLayout, false);
		mainLayout.addView(buttonLayout);
	}
 
}

As you can see, the second parameter of the inflate() method is specified as mainLayout. That is, we specified a parent layout for the button_layout.xml layout. In this case, the layout_width and layout_height attributes can take effect.

Re-run the program, the effect is as shown in the figure below:

So far, we have explained the role of the second parameter root of the inflate() method very clearly. Then there is another question, what does the third parameter attachToRoot mean?

Pay attention to the above code, we specify the second parameter as mainLayout and at the same time specify the third parameter as false. If you try to specify the third parameter as true, and then re-run the code, the program will simply crash. The crash information is as follows:

This crash message is saying that we are adding a child View, but this child View already has a parent layout, and the parent layout needs to call removeView() to remove the child View before adding it.

Why does such an error occur after modifying the third parameter? Let's analyze it now.

First pay attention to what the name of the third parameter is, attachToRoot. Literally, it is asking if we want to add to root. So what is root? Observe the definition of the inflate() method again, and you will find that the second parameter is not root?

public View inflate(int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    
    
    ...
}

In other words, the meaning of attachToRoot is to ask us whether to add the currently loaded xml layout to the parent layout passed in by the second parameter. If you pass in true, it means it will be added, and if you pass in false, it means it won't be added.

So in the code just now, we first pass false in the third parameter of the inflate() method, then the button_layout.xml layout will not be added to the mainLayout, and we can manually call the addView() method later Add it to mainLayout.

And if you change the third parameter to true, it means that the button_layout.xml layout has been automatically added to the mainLayout. At this time, call the addView() method again and find that button_layout.xml already has a parent layout, so it will naturally throws the above exception.

After such an explanation, do you have a clear understanding of the role of each parameter in the inflate() method?

In fact, after understanding this, we can go back and look at the code written in the past. For example, everyone must have used Fragment. To load a layout in Fragment, we usually write like this:

public class MyFragment extends Fragment {
    
    

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, 
                             @Nullable ViewGroup container, 
                             @Nullable Bundle savedInstanceState) {
    
    
        return inflater.inflate(R.layout.fragment_layout, container, false);
    }
}

I don’t know if you have ever thought about it in the past, why the last parameter of the inflate() method must be passed in false?

So now it's time to think about it. Take a look at the relevant source code of Fragment, and you will find that it will add the View we returned in the onCreateView() method to a Container:

void addViewToContainer() {
    
    
    // Ensure that our new Fragment is placed in the right index
    // based on its relative position to Fragments already in the
    // same container
    int index = mFragmentStore.findFragmentIndexInContainer(mFragment);
    mFragment.mContainer.addView(mFragment.mView, index);
}

This situation is very similar to our previous example, that is to say, the subsequent Fragment itself will have an addView operation. If we pass the third parameter of the inflate() method to true, then the layout from the inflate will be directly added into the parent layout. In this way, when you addView again later, you will find that it already has a parent layout, thus throwing the same crash information as above.

If you don't believe me, you can try it yourself.

In addition to Fragment, the usage of LayoutInflater in RecyclerView is also based on the same reason, so it will not be discussed here.

I hope that after reading this article, you can have some new understanding of LayoutInflater.


If you want to learn Kotlin and the latest Android knowledge, you can refer to my new book "The First Line of Code 3rd Edition" , click here to view details .


Pay attention to my technical public account, and high-quality technical articles will be pushed every day.

Scan the QR code below on WeChat to follow:

Guess you like

Origin blog.csdn.net/sinyu890807/article/details/121889703