Question

While debugging a custom view overriden onMeasure() method I see that there are several calls to this method.
I am only dealing with the height of the view, leaving the width spec always unchanged.
At some point I get a call with the (height) MeasureSpec getMode() == EXACTLY and getSize() == 0.
This does not make sense and contradicts the Android documentation:

MeasureSpecs are used to push requirements down the tree from parent to child.
A MeasureSpec can be in one of three modes:

UNSPECIFIED: This is used by a parent to determine the desired dimension of a child
view. For example, a LinearLayout may call measure() on its child with the height set
to UNSPECIFIED and a width of EXACTLY 240 to find out how tall the child view wants
to be given a width of 240 pixels.

EXACTLY: This is used by the parent to impose an exact size on the child. The child
must use this size, and guarantee that all of its descendants will fit within this 
size.

AT_MOST: This is used by the parent to impose a maximum size on the child. The child
must guarantee that it and all of its descendants will fit within this size.

If I do as supposed (The child must use this size), setMeasureDimension(specWidth, sepcHeight) I get an exception telling the width and height of a view must be > 0.
I suspect that this call is caused because in the layout XML, the view has layout_weight="1" and, according to the documentation suggestions:

To create a linear layout in which each child uses the same amount of space on the screen, set the android:layout_height of each view to "0dp"

But still, when MeasureSpec mode is EXACTLY, the size should be > 0. Or at least there should be some rule to follow in these cases, in the documentation.

this is the code:

    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int specHeight = MeasureSpec.getSize(heightMeasureSpec);
    int specWidth = MeasureSpec.getSize(widthMeasureSpec);

    int desiredHeight = Math.max(BOX_MIN_HEIGHT, HSVColorPickerPreference.this.boxHeight);

    int chosenHeight = 0;

    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    if( heightMode == MeasureSpec.UNSPECIFIED ) {
        chosenHeight = desiredHeight;
    } else if( heightMode == MeasureSpec.AT_MOST ) {
        chosenHeight = Math.min(specHeight, desiredHeight);
    } else if( heightMode == MeasureSpec.EXACTLY ) {
        chosenHeight = specHeight;
    }

    setMeasuredDimension(specWidth, chosenHeight);

This is the log, note the last calls to onMeasure():

03-29 08:17:13.388: D/ValueSlider(1384): + onMeasure(widthMeasureSpec:1073742403, heightMeasureSpec:-2147483435)
03-29 08:17:13.388: W/ValueSlider(1384): MeasureSpec AT_MOST, specSize=213, desiredSize=40, chosenSize=40
03-29 08:17:13.388: D/AlphaSlider(1384): + onMeasure(widthMeasureSpec:1073742403, heightMeasureSpec:-2147483435)
03-29 08:17:13.388: W/AlphaSlider(1384): MeasureSpec AT_MOST, specSize=213, desiredSize=40, chosenSize=40
03-29 08:17:13.388: D/ValueSlider(1384): + onMeasure(widthMeasureSpec:1073742403, heightMeasureSpec:1073741824)
03-29 08:17:13.388: W/ValueSlider(1384): MeasureSpec EXACTLY, specSize=0, desiredSize=40, chosenSize=0
03-29 08:17:13.388: D/AlphaSlider(1384): + onMeasure(widthMeasureSpec:1073742403, heightMeasureSpec:1073741824)
03-29 08:17:13.388: W/AlphaSlider(1384): MeasureSpec EXACTLY, specSize=0, desiredSize=40, chosenSize=0
03-29 08:17:13.508: D/ValueSlider(1384): + onSizeChanged(w:579, h:0, oldw:0, oldh:0)
03-29 08:17:13.508: D/AndroidRuntime(1384): Shutting down VM
03-29 08:17:13.508: W/dalvikvm(1384): threadid=1: thread exiting with uncaught exception (group=0xb2fe0180)
03-29 08:17:13.518: E/AndroidRuntime(1384): FATAL EXCEPTION: main
03-29 08:17:13.518: E/AndroidRuntime(1384): java.lang.IllegalArgumentException: width and height must be > 0
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.graphics.Bitmap.createBitmap(Bitmap.java:603)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.graphics.Bitmap.createBitmap(Bitmap.java:585)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at com.UturpatShuPepper.lib.HSVColorPickerPreference$Slider.onSizeChanged(HSVColorPickerPreference.java:962)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.View.setFrame(View.java:11361)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.View.layout(View.java:11272)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.widget.FrameLayout.onLayout(FrameLayout.java:431)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.View.layout(View.java:11278)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.ViewGroup.layout(ViewGroup.java:4224)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1628)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1486)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.widget.LinearLayout.onLayout(LinearLayout.java:1399)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.View.layout(View.java:11278)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.ViewGroup.layout(ViewGroup.java:4224)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.widget.RelativeLayout.onLayout(RelativeLayout.java:925)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.View.layout(View.java:11278)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.ViewGroup.layout(ViewGroup.java:4224)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.widget.FrameLayout.onLayout(FrameLayout.java:431)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.View.layout(View.java:11278)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.ViewGroup.layout(ViewGroup.java:4224)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.widget.FrameLayout.onLayout(FrameLayout.java:431)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.View.layout(View.java:11278)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.ViewGroup.layout(ViewGroup.java:4224)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1628)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1486)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.widget.LinearLayout.onLayout(LinearLayout.java:1399)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.View.layout(View.java:11278)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.ViewGroup.layout(ViewGroup.java:4224)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.widget.FrameLayout.onLayout(FrameLayout.java:431)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.View.layout(View.java:11278)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.ViewGroup.layout(ViewGroup.java:4224)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.widget.FrameLayout.onLayout(FrameLayout.java:431)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.View.layout(View.java:11278)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.ViewGroup.layout(ViewGroup.java:4224)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.widget.FrameLayout.onLayout(FrameLayout.java:431)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.View.layout(View.java:11278)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.ViewGroup.layout(ViewGroup.java:4224)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1489)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.view.ViewRootImpl.handleMessage(ViewRootImpl.java:2442)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.os.Handler.dispatchMessage(Handler.java:99)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.os.Looper.loop(Looper.java:137)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at android.app.ActivityThread.main(ActivityThread.java:4424)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at java.lang.reflect.Method.invokeNative(Native Method)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at java.lang.reflect.Method.invoke(Method.java:511)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
03-29 08:17:13.518: E/AndroidRuntime(1384):     at dalvik.system.NativeStart.main(Native Method)
Was it helpful?

Solution

Since I finally understood what's going on, I leave here the answer for future reference:

Android is doing what it has to do when measuring the UI components.
If the user (me in this case) does not follow the simple rules, then EXACTLY 0 may happen.
It can be made harmless if you just check for 0 size in the onSizeChanged() method. But even better if you avoid mixing measure modes, like I did. Explanation follows.

I defined in XML weighted views (using layout_weight). Those were the custom views mentioned in the question. My mistake was to also try to require a specific height for the views in

@Override 
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

the culprit are the lines

int desiredHeight = Math.max(BOX_MIN_HEIGHT, HSVColorPickerPreference.this.boxHeight);

. . . 

    chosenHeight = Math.min(specHeight, desiredHeight);

. . .

This clashes head to head with the heuristic for weighted layout. Why? Let's take for example 3 widgets with weight=1, and one of them is behaving bad, as described above.

When LineraLayout makes a first pass over its children, it lets them get wild and ask for any size they want. In our example 2 widgets will ask for as much as possible, the custom widget will ask for something modest, less than the maximum.

The second pass is the killer, LinearLayout does not know one of the weighted widgets asked for less than it is supposed to, all in all, it has a weight defined for it. LinearLayout looks at the total requested measure form pass one and sees it is more than it has to give. Then it calculates the delta overflow and makes another pass distributing the overflow among the weighted widgets. Consequently the custom view widget has to cut back more than it requested, leaving it at 0 size.

The situation is analog to going for a beer with friends. You order one beer and your friends order beer, fries, chasers, the lot. At the end of the evening the check is split equally among everybody, you end up paying more than the beer you had. So did my custom view.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top