Android layout optimization three swordsman

Foreword

When writing an Android layout, you will always encounter pain points of one kind or another, such as:

  1. Some layouts are used on many pages, and the styles are the same. Every time you use them, you have to copy and paste a large number. Is there any way to reuse them?
  2. After solving the problem in 1 , I found that there is always a layer of layout on the outside of the multiplexed layout. It is necessary to know that layout nesting is a trick that affects performance;
  3. Some layouts will only be displayed when they are used, but they must be written in advance, although they are set toInvisibleorGone, still more or less will take up memory.

To solve these pain points, we can ask the Android layout to optimize the three swordsmen out of the code, they areincludeMerge andViewStubThree tags, now let's get to know them. Before we do this, let's take a look at the interface effect of our project:

效果完全版

The interface is not complicated, let's implement it one by one.

1、include

The Chinese meaning of include is "include" and "include" when you use it in a main page.When the include tag is used, it means that the current main layout contains the layout in the label, so that the effect of multiplexing the layout is well achieved. Using it on top of common layouts such as title bars and split lines can greatly reduce the amount of code. It has two main attributes:

  1. Layout: Required attribute, for the layout name you need to insert the current main layout, throughR.layout.xx的方式引用;
  2. Id: This property can be used when you want to set an id for a layout added via include, which overrides the layout id inserted into the main layout.

Let's take a look at it.

1.1 General use

Let's create one firstViewOptimizationActivity, and then create a layout_include.xml layout file, its content is very simple, just a TextView:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:gravity="center_vertical"
    android:textSize="14sp"
    android:background="@android:color/holo_red_light"
    android:layout_height="40dp">

</TextView>

Now we will useInclude tag to add it toIn the layout of ViewOptimizationActivity:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    Android:orientation="vertical"
    Tools:context=".ViewOptimizationActivity">

    <!--include tag use -->
    <TextView
        Android:textSize="18sp"
        Android:text="1, use of include tags"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content" />

    <include
        Android:id="@+id/tv_include1"
        Layout="@layout/layout_include"/>

</LinearLayout>

That's right,The use of include is as simple as specifying the layout id to include. In addition, we give thisThe include tag sets an id, in order to verify that it is the root layout of the layout_include.xml TextView id, we areInitialize the TextView in ViewOptimizationActivity and set the text to it:

TextView tvInclude1 = findViewById(R.id.tv_include1);
        tvInclude1.setText("1.1 include layout under regular");

After running, you can see the following layout:

include常规使用

Explain that the layout and id we set are all successful. However, you may have questions about the id attribute: id I can set it directly in the TextView, why rewrite it? Don't forget that our purpose is to reuse when you use it in a main layoutWhen the include tag adds more than two of the same layout, the id is the same, so rewriting it allows us to call it and its controls better. There is another situation, if your main layout isRelateLayout, in order to set the relative position, you also need to set a different id.

1.2 Rewriting the layout properties of the root layout

In addition to id, we can also rewrite width, margin, and visibility (Visibility) theseLayout property . But be sure to pay attention, just rewriteAndroid:layout_height orAndroid: layout_width is not OK, you must have two simultaneous rewrites to work. The same is true for margins, if we want to add a right margin to a layout that is included in the include:

    <include
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:layout_marginEnd="40dp"
        android:id="@+id/tv_include2"
        layout="@layout/layout_include"/>

After setting a paragraph of text after initialization, you can see the following effects:

设置了右边距的include布局

It can be seen that 1.2 is obviously one more right margin than 1.1.

1.3 Processing when the control ID is the same

In 1.1 we know that the id attribute can be rewritten.Include The root layout id of the layout, but there is nothing to do with the layout and controls in the root layout. If a layout is included multiple times in the main layout, how do you distinguish the controls inside?

We first create a layout of layout_include2.xml, its root layout isFrameLayout, there is aTextView, its id is tv_same:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:background="@android:color/holo_orange_light"
    android:layout_height="wrap_content">

    <TextView
        android:gravity="center_vertical"
        android:id="@+id/tv_same"
        android:layout_width="match_parent"
        android:layout_height="50dp" />

</FrameLayout>

Added in the main layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    Android:orientation="vertical"
    Tools:context=".ViewOptimizationActivity">

    <!--include tag use -->
    ......

    <include layout="@layout/layout_include2"/>

    <include
        Android:id="@+id/view_same"
        Layout="@layout/layout_include2"/>

</LinearLayout>

For the sake of distinction, the id is set for the second layout_include2. Maybe you have already reacted, yes, we just want to create the root layout of the object, and then initialize the controls inside:

TextView tvSame = findViewById(R.id.tv_same);
        tvSame.setText("1.3 Here the ID of the TextView is tv_same");
        FrameLayout viewSame = findViewById(R.id.view_same);
        TextView tvSame2 = viewSame.findViewById(R.id.tv_same);
        tvSame2.setText("1.3 The TextView ID here is also tv_same");

You can see this effect after running:设置了右边距的include布局

It can be seen that although the id of the control is the same, there is no conflict in use.

2、merge

The include tag solves the problem of layout reuse, but it also brings another problem: layout nesting. Because putting the layout that needs to be reused into a sub-layout must add a root layout, if the root layout of your main layout is the same as the root layout you need to include (for example,LinearLayout), then it is equivalent to adding a layer of extra layout in the middle. So is there a way to use it?Does include not increase the layout level? The answer is of course yes, that is to useMerge tag.

useThe merge tag should pay attention to one point: it must be the root node in a layout file, it looks no different from other layouts, but it is special in that it does not draw when the page is loaded. For example, it is like a porter of a layout or control. After moving the "goods" to the main layout, it will retreat without taking up any space, so it will not increase the layout level. This is just like its name, it only acts as a "merger."

2.1 merge regular use

Let's verify that we first create a layout_merge.xml and use it at the root node.Mergelabel:

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

    <TextView
        Android:id="@+id/tv_merge1"
        Android:text="I am TextView1 in merge"
        Android:background="@android:color/holo_green_light"
        Android:gravity="center"
        Android:layout_width="wrap_content"
        Android:layout_height="40dp" />

    <TextView
        Android:layout_toEndOf="@+id/tv_merge1"
        Android:id="@+id/tv_merge2"
        Android:text="I am TextView2 in merge"
        Android:background="@android:color/holo_blue_light"
        Android:gravity="center"
        Android:layout_width="match_parent"
        Android:layout_height="40dp" />
</merge>

Here I used some relative layout properties, for the reason you will know later. We are going onAdd layout to ViewOptimizationActivity的布局添加RelativeLayout, then useThe include tag adds layout_merge.xml to it:

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <include
            android:id="@+id/view_merge"
            layout="@layout/layout_merge"/>
    </RelativeLayout>

Run the effect diagram:

merge常规使用

2.2 The impact of the merge tag on the layout level

In layout_merge.xml we use relative layout propertiesAndroid:layout_toEndOfSet the blue TextView to the right of the green TextView, and the parent layout of layout_merge.xml isRelativeLayout, so this property is working,The merge tag does not affect the controls inside, nor does it increase the layout level.

If you are not at ease, you can check it with Android Studio. The Android Studio I use is version 3.1 and can be passed.Layout InspectorView the layout level, but remember to run the project on a real machine or simulator. Click Tools-Layout Inspector, then select the Activity you want to view, you can see the following level diagram:

布局层级

can be seenRelativeLayoutThere are two TextViews directly below,The merge tag does not increase the layout level. It can also be seen from hereThe limitation of merge, you need to be clearMerge inside the layout and controlsIncludeWhich type of layout can be set in advanceThe layout of the merge and || inside the control.里面的布局和控件的位置。

2.3 merge ID

studyingWhen we include tags, we know that it'sThe android:id attribute can override the root layout id of the included, but if the root node isMerge? Said beforeMerge is not drawn as a layout, so setting the id to it doesn't work. We can have a parent layout in itAdd a TextView to the RelativeLayout, useThe android:layout_below property is set to layout_merge below:

<RelativeLayout
        Android:layout_width="match_parent"
        Android:layout_height="wrap_content">

        <include
            Android:id="@+id/view_merge"
            Layout="@layout/layout_merge"/>

        <TextView
            Android:text="I am not the layout in the merge"
            Android:layout_below="@+id/view_merge"
            Android:background="@android:color/holo_purple"
            Android:gravity="center"
            Android:layout_width="match_parent"
            Android:layout_height="40dp"/>
    </RelativeLayout>

After running, you will find that the newly added TextView will cover the merge layout, not below it as expected. If putThe id in android:layout_below is changed to the id of any TextView in layout_merge.xml (such as tv_merge1). After running, you can see the following effects:

This also corresponds to the situation in 2.2, the parent layoutRelativeLayoutThe lower level layout is the TextView that is included.

3、ViewStub

You must have encountered a situation where some layouts in the page do not need to be displayed during initialization, but they have to be written in the layout file beforehand, although the settings areInvisibleorGone, but it will still load during initialization, which will undoubtedly affect the page loading speed. In response to this situation, Android provides us with a weapon --ViewStub. This is an invisible, size 0 view with lazy loading, which exists in the view hierarchy, but only insetVisibility() andThe inflate() method call will only populate the view, so it will not affect the initialization load speed. It has three important attributes:

  • Android:layout:ViewStub needs to fill the view name, which is in the form of "R.layout.xx";
  • Android:inflateId: Overrides the parent layout id of the filled view.

versusThe include tags are different,ViewStubAndroid:idproperties are setViewStubIt's id instead of overriding the layout id, don't make a mistake. In addition,ViewStub is also availableThe OnInflateListener interface is used to monitor whether the layout has been loaded.

3.1 The correct way to fill the layout

Let's create a layout_view_stub.xml and place aSwitchSwitch:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:background="@android:color/holo_blue_dark"
    android:layout_height="100dp">
    <Switch
        android:id="@+id/sw"
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</FrameLayout>

Then modify the layout of the Activity as follows:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    Android:orientation="vertical"
    Tools:context=".ViewOptimizationActivity">

    <!--Use of ViewStub tag -->
    <TextView
        Android:textSize="18sp"
        Android:text="3, the use of ViewStub tags"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content" />

    <ViewStub
        Android:id="@+id/view_stub"
        Android:inflatedId="@+id/view_inflate"
        Android:layout="@layout/layout_view_stub"
        Android:layout_width="match_parent"
        Android:layout_height="100dp" />
    <LinearLayout
        Android:orientation="horizontal"
        Android:layout_width="match_parent"
        Android:layout_height="wrap_content">

        <Button
            Android:text="display"
            Android:id="@+id/btn_show"
            Android:layout_weight="1"
            Android:layout_width="0dp"
            Android:layout_height="wrap_content" />

        <Button
            Android:text="hidden"
            Android:id="@+id/btn_hide"
            Android:layout_weight="1"
            Android:layout_width="0dp"
            Android:layout_height="wrap_content" />

        <Button
            Android:text="Action Parent Layout Control"
            Android:id="@+id/btn_control"
            Android:layout_width="wrap_content"
            Android:layout_height="wrap_content" />
    </LinearLayout>
</LinearLayout>

inListening in ViewOptimizationActivity中监听Fill event for ViewStub:

viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
            @Override
            Public void onInflate(ViewStub viewStub, View view) {
                Toast.makeText(ViewOptimizationActivity.this, "ViewStub loaded", Toast.LENGTH_SHORT).show();
            }
        });

Then fill and display the layout_view_stub with the button event:

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_show:
                viewStub.inflate();
                break;
            case R.id.btn_hide:
                viewStub.setVisibility(View.GONE);
                break;
            default:
                break;
        }
    }

After running, click the "Show" button, layout_view_stub is displayed, and the "ViewStub loaded" Toast pops up; click the "Hide" button, the layout is hidden again, but click the "Show" button again, the page actually flashes back. , look at the log and find that an exception was thrown:

java.lang.IllegalStateException: ViewStub must have a non-null ViewGroup viewParent

We openViewStub source code to see where this exception is thrown. Soon we can be positioned to beInflate()Method

    public View inflate() {
        final ViewParent viewParent = getParent();

        if (viewParent != null && viewParent instanceof ViewGroup) {
            if (mLayoutResource != 0) {
                final ViewGroup parent = (ViewGroup) viewParent;
                final View view = inflateViewNoAdd(parent);
                replaceSelfWithView(view, parent);

                mInflatedViewRef = new WeakReference<>(view);
                if (mInflateListener != null) {
                    mInflateListener.onInflate(this, view);
                }

                return view;
            } else {
                throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
            }
        } else {
            throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
        }
    }

NoticeThere is one in the if statementThe replaceSelfWithView() method, listening to this name gives people an ominous premonition, click inside to see:

    private void replaceSelfWithView(View view, ViewGroup parent) {
        final int index = parent.indexOfChild(this);
        parent.removeViewInLayout(this);

        final ViewGroup.LayoutParams layoutParams = getLayoutParams();
        if (layoutParams != null) {
            parent.addView(view, index, layoutParams);
        } else {
            parent.addView(view, index);
        }
    }

really,ViewStub is called hereThe removeViewInLayout() method removes itself from the layout. We will understand here,ViewStub will self-destruct after filling the layout successfully, call againThe inflate() method throws an IllegalStateException. At this point, if you want to display the layout again, you can callsetVisibility()method.

in order to avoidThe inflate() method is called multiple times. We can use the following three methods:

3.1.1 Capture exception

We can catch exceptions while callingThe setVisibility() method displays the layout.

                try {
                    viewStub.inflate();
                } catch (IllegalStateException e) {
                    Log.e("Tag",e.toString());
                    view.setVisibility(View.VISIBLE);
                }

3.1.2 By listening to the Fill event of the ViewStub

Declare a Boolean variableisViewStubShow, the default value is false, after the layout is filled successfully, the listen eventSet it to true in the onInflate method.

                if (isViewStubShow){
                    viewStub.setVisibility(View.VISIBLE);
                }else {
                    viewStub.inflate();
                }

3.1.3 Directly calling the setVisibility() method

Let me take a look first.In ViewStub中的setVisibility()Source:

    public void setVisibility(int visibility) {
        if (mInflatedViewRef != null) {
            View view = mInflatedViewRef.get();
            if (view != null) {
                view.setVisibility(visibility);
            } else {
                throw new IllegalStateException("setVisibility called on un-referenced view");
            }
        } else {
            super.setVisibility(visibility);
            if (visibility == VISIBLE || visibility == INVISIBLE) {
                inflate();
            }
        }
    }

Can see that atInflate()InitializationBefore mInflatedViewRef, if setVisibilityVISIBLE will be calledInflate()method, inmInflatedViewRef will not be called after it is not nullInflate()了。

3.2 Why is viewStub.getVisibility() always equal to 0?

When displaying the layout in ViewStub, you might take the following approach:

                if (viewStub.getVisibility() == View.GONE){
                    viewStub.setVisibility(View.VISIBLE);
                }else {
                    viewStub.setVisibility(View.GONE);
                }

Congratulations, step on a big pit. Write this way and you will find that after clicking the "Show" buttonThe layout inside ViewStub will not be displayed again, which means that the code in the if statement is not executed. If you willThe value of viewStub.getVisibility() is printed and you will see that it is always 0, which is exactlyThe value of View.VISIBLE. Strange, we clearly wroteviewStub.setVisibility(View.GONE), layout_view_stub is also hidden, whyIs the status of ViewStub still visible?

Go back to 3.1.3 and seeIn ViewStub中的setVisibility()source code, first determine the weak reference objectmInflatedViewRef Whether it is empty, if it is not empty, then take out the object stored, that is, weView in ViewStub, then call viewsetVisibility()method,When mInflatedViewRef is empty, it is judgedVisibilityVISIBLE orCalled when INVISIBLE时调用Inflate() method fills the layout ifGONE will not be processed. In this way, inmInflatedViewRef is not empty, that is, if the layout has been filled,In ViewStub中的The setVisibility() method actually sets the visibility of the internal view instead ofViewStub itself. This design is also in line withThe feature of ViewStub, that is, after the layout is filled, it is self-destructed, and it makes no sense to set visibility for it.

3.3 Operating Layout Controls

Compare it carefully, actuallyViewStubIt’s like a lazyInclude, we need to load it when it loads. To manipulate the controls inside the layout,Like include, you can initialize firstReinitialize the control in the layout in ViewStub:

//1, initialize the initlate layout and then initialize the controls.
                FrameLayout frameLayout = findViewById(R.id.view_inflate);//android:inflatedId set id
                Switch sw = frameLayout.findViewById(R.id.sw);
                Sw.toggle();

If the id of the control in the main layout does not conflict, you can directly initialize the control to use:

//2, directly initialize the control
                Switch sw = findViewById(R.id.sw);
                Sw.toggle();

Ok, aboutThe knowledge of ViewStub is so much.

postscript

Originally thought that knowledge is not difficult, it should be able to write faster, I did not expect to write on and off for four or five days, I feel a little tired to write. I hope that I can still help you a bit, and I am still looking for corrections. Let's use the mind map to total it and give it the source code on GitHub.

思维导图

Reference article