Search code examples
androidandroid-custom-viewonmeasure

onMeasure and onLayout of child views are not called after requestLayout on their parent


I'm trying understanding how Android measures and lays out views. I created a sample app with custom views and view groups.

CustomViewGroup

public class CustomViewGroup extends ViewGroup {

    private static final String LOG_TAG = "CustomViewGroup";

    public CustomViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.d(LOG_TAG, String.format(Locale.ENGLISH,
                getTag() + " " +
                        "onMeasure(widthMeasureSpec=%d, heightMeasureSpec%d)",
                widthMeasureSpec, heightMeasureSpec
        ));

        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                // Make or work out measurements for children here (MeasureSpec.make...)
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        Log.d(LOG_TAG, String.format(Locale.ENGLISH,
                getTag() + " " +
                        "onLayout(changed=%b, left=%d, top=%d, right=%d, bottom=%d)",
                changed, left, top, top, right, bottom
        ));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.d(LOG_TAG, getTag() + " " + "onDraw");
    }
}

CustomView

public class CustomView extends View {

    private static final String LOG_TAG = "CustomView";

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        Log.d(LOG_TAG, String.format(Locale.ENGLISH,
                getTag() + " " +
                "onLayout(changed=%b, left=%d, top=%d, right=%d, bottom=%d)",
                changed, left, top, top, right, bottom
        ));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.d(LOG_TAG, getTag() + " " +"onDraw");
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.d(LOG_TAG, String.format(Locale.ENGLISH,
                getTag() + " " +
                "onMeasure(widthMeasureSpec=%d, heightMeasureSpec=%d)",
                widthMeasureSpec, heightMeasureSpec
        ));
    }
}

MainActivity

public class MainActivity extends AppCompatActivity {

    private View mCustomGroup1;
    private View mCustomGroup2;
    private View mContainer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.d("MainAct", "onCreate");
        mCustomGroup1 = findViewById(R.id.requestLayout);
        mCustomGroup2 = findViewById(R.id.requestLayout2);
        mContainer = findViewById(R.id.activity_main);

        findViewById(R.id.requestLayoutBtn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mCustomGroup1.requestLayout();
            }
        });

        findViewById(R.id.requestLayoutBtn2).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mCustomGroup2.requestLayout();
            }
        });

        findViewById(R.id.requestLayoutContBtn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mContainer.requestLayout();
            }
        });

    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="ru.dmitriev.squareorderedlayout.MainActivity">

    <Button
        android:id="@+id/requestLayoutBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="requestLayout" />

    <Button
        android:id="@+id/requestLayoutBtn2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="requestLayout2" />

    <Button
        android:id="@+id/requestLayoutContBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="requestLayoutCont" />

    <ru.dmitriev.squareorderedlayout.CustomViewGroup
        android:id="@+id/requestLayout"
        android:layout_width="100dp"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="#7f7"
        android:tag="CustomVieGroup1">

        <ru.dmitriev.squareorderedlayout.CustomView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:tag="CustomView11" />

        <ru.dmitriev.squareorderedlayout.CustomView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:tag="CustomView12" />
    </ru.dmitriev.squareorderedlayout.CustomViewGroup>

    <ru.dmitriev.squareorderedlayout.CustomViewGroup
        android:id="@+id/requestLayout2"
        android:layout_width="100dp"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="#f77"
        android:tag="CustomVieGroup2">

        <ru.dmitriev.squareorderedlayout.CustomView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:tag="CustomView21" />

        <ru.dmitriev.squareorderedlayout.CustomView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:tag="CustomView22" />

    </ru.dmitriev.squareorderedlayout.CustomViewGroup>
</LinearLayout>

When the activity starts, onMeasure, onLayout and onDraw are called on the view groups tagged CustomVieGroup1 and CustomVieGroup2. Each view group calls these methods on their children. It's OK. I understand it.

When I call requestLayout on the view group tagged CustomVieGroup1, onMeasure, onLayout and onDraw are called on CustomVieGroup1. The group calls these methods on their children. I understand it.

When I call requestLayout on the view group tagged CustomVieGroup2, onMeasure, onLayout and onDraw are called on CustomVieGroup2. The group calls these methods on their children. I understand it as well.

But when I call requestLayout on the view with id activity_main, onMeasure and onLayout are not called on CustomVieGroup1 or CustomVieGroup2. Why? I expected that onMeasure, onLayout and onDraw will be called on both CustomVieGroup1 and CustomVieGroup2 (i.e. the cgildren of the view with id activity_main)

According to the documentation of requestLayout:

This will schedule a layout pass of the view tree.


Solution

  • requestLayout() will cause the onLayout() method of the LinearLayout (@id/activity_main) to execute. However, there is no guarantee that the LinearLayout will then re-layout its children; it really depends on its implementation. For example, your LinearLayout width/height is set to match_parent. When the LinearLayout is laid-out a second time, its width and height wont change because its size it is based on its parent size, not its children. Because its layout is not based on its children, there is no need for LinearLayout to re-layout its children. This is why invalidating a layout of nearly any ViewGroup whose size matches its parent will not cause that layout's children to be laid-out.

    You should be able to confirm this be changing your LinearLayout (@id/activity_main) to 'wrap_content'. By doing this, the layout's size becomes dependent on its children, so it will need to re-layout its children. This will cause your custom view layout methods to be called. Hope this helps,