Maksim Dmitriev Maksim Dmitriev - 27 days ago 15
Android Question

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.

Answer

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,

Comments