androidandroid-custom-viewandroid-constraintlayoutandroid-wrap-content

MeasureSpecs passed to onMeasure indicate the full parent size instead of the remaining available space


I'm observing an issue where the onMeasure method of my custom view is receiving a MeasureSpec that corresponds to the full parent view's size, instead of the remaining available space. This happens when the view's layout_width/height attributes are equal to wrap_content. This issue prevents me from calculating the actual custom view's size correctly.

The issue is best explained by showing some minimal reproducible example code, and stating the actual vs. the expected results.

I have a CustomView widget that is simply a red-bordered rectangle. I overrode its onMeasure method like this:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    // Log the parameters to debug
    val widthSpecString = MeasureSpec.toString(widthMeasureSpec)
    val heightSpecString = MeasureSpec.toString(heightMeasureSpec)
    Log.d("------", "width=$widthSpecString, height=$heightSpecString")

    val width = MeasureSpec.getSize(widthMeasureSpec)
    val height = MeasureSpec.getSize(heightMeasureSpec)
    setMeasuredDimension(width, height)
}

(NOTE: I know this is an extremely oversimplified onMeasure method, but remember this is just a minimal reproducible example, and I'm more interested in the widthMeasureSpec and heightMeasureSpec parameters passed in.)

I have the following layout:

<androidx.constraintlayout.widget.ConstraintLayout 
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  <View
    android:id="@+id/topView"
    android:layout_width="0dp"
    android:layout_height="300px"
    android:background="@color/gray_transparent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent"
  />
  <com.customviewsizedemo.CustomView
    android:id="@+id/customView"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toBottomOf="@id/topView"
    app:layout_constraintBottom_toBottomOf="parent"
  />
</androidx.constraintlayout.widget.ConstraintLayout>

which produces the following screenshot

enter image description here

and the following debug logs:

D/------: width=MeasureSpec: EXACTLY 1080, height=MeasureSpec: EXACTLY 1706
D/------: width=MeasureSpec: EXACTLY 1080, height=MeasureSpec: EXACTLY 1706
D/------: width=MeasureSpec: EXACTLY 1080, height=MeasureSpec: EXACTLY 1706
D/------: width=MeasureSpec: EXACTLY 1080, height=MeasureSpec: EXACTLY 1706

So far so good. From the logs, we can see that the area that the red-bordered rectangle occupies is 1706 pixels high. Since the gray view on top of it is 300 pixels high, we also know that the whole screen's height is 1706 + 300 = 2006 pixels high.

Now the interesting part: if I change the layout such that the custom view's layout_width and layout_height is wrap_view, something weird happens. That is, if I change the layout like this:

<androidx.constraintlayout.widget.ConstraintLayout 
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  <View
    android:id="@+id/topView"
    ...
  />
  <com.customviewsizedemo.CustomView
    android:id="@+id/customView"
    android:layout_width="wrap_content"  <----------------- Change here
    android:layout_height="wrap_content" <----------------- Change here
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toBottomOf="@id/topView"
    app:layout_constraintBottom_toBottomOf="parent"
  />
</androidx.constraintlayout.widget.ConstraintLayout>

we get this screenshot (observe that the bottom border of the rectangle is now offscreen):

enter image description here

And we get these logs:

D/------: width=MeasureSpec: AT_MOST 1080, height=MeasureSpec: AT_MOST 2006
D/------: width=MeasureSpec: AT_MOST 1080, height=MeasureSpec: AT_MOST 2006

THE ISSUE: heightMeasureSpec is equal to AT_MOST 2006. This doesn't make sense, because actually, the red-bordered rectangle should be at most 1706 pixels high. The gray view on top of it hasn't gone away: it's still occupying space that should be respected, but it doesn't seem to be respected/accounted for.

Why is the heightMeasureSpec equal to the full parent (screen) height? What can I do so I receive the correct value in the onMeasure method?

In case it's needed, here's the full minimal reproducible example (MRE): https://github.com/HectorRicardo/CustomViewSizeDemo


Solution

  • Specifying wrap_content on a view does not, by default, constrain the view to its constraints. Your XML will wrap the content of the custom view and center the result between the top and bottom constraints. Centering will place the top of the view above its top constraint and below its bottom constraint. What you will see in logcat of a Nexus 6 API 31 emulator is the following.

    width=MeasureSpec: AT_MOST 1440, height=MeasureSpec: AT_MOST 2112
    width=MeasureSpec: AT_MOST 1440, height=MeasureSpec: AT_MOST 2112

    If you want ConstraintLayout to honor the top and bottom constraints on your custom view you will need to add the following to the XML for the custom view:

    app:layout_constrainedHeight="true"
    

    With this added, you will see the following:

    width=MeasureSpec: AT_MOST 1440, height=MeasureSpec: AT_MOST 2112
    width=MeasureSpec: AT_MOST 1440, height=MeasureSpec: EXACTLY 1812
    width=MeasureSpec: AT_MOST 1440, height=MeasureSpec: AT_MOST 2112
    width=MeasureSpec: AT_MOST 1440, height=MeasureSpec: EXACTLY 1812

    This is what displays:

    enter image description here

    Which, I believe, is what you expected. Also, as you state, you could simply set the view's height to 0dp.