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
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):
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
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:
Which, I believe, is what you expected. Also, as you state, you could simply set the view's height to 0dp
.