onMeasure(): wrap_content, how do I know the size to wrap?

15,295

Solution 1

If you can't measure the bitmap prior to the onMeasure call, then you could return a size of zero until the Bitmap is loaded. Once it is loaded, invalidate the parent ViewGroup to force another measure (can't remember if invalidate() on the View itself will force an onMeasure).

Solution 2

This is the order that these commonly used view methods are run in:

1. Constructor    // choose your desired size 
2. onMeasure      // parent will determine if your desired size is acceptable
3. onSizeChanged
4. onLayout       
5. onDraw         // draw your view content at the size specified by the parent

Choosing a desired size

If your view could be any size it wanted, what size would it choose? This will be your wrap_content size and will depend on the content of your custom view. Examples:

  • If your custom view is an image, then your desired size would probably be the pixel dimensions of the bitmap plus any padding. (It is your responsibility to figure padding into your calculations when choosing a size and drawing the content.)
  • If you custom view is an analog clock, then the desired size could be some default size that it would look good at. (You can always get the the dp to px size for the device.)

If your desired size uses heavy calculations, then do that in your constructor. Otherwise, you can just assign it in onMeasure. (onMeasure, onLayout, and onDraw may be called multiple times so that is why it isn't good to do heavy work here.)

Negotiating the final size

onMeasure is the place where the child tells the parent how big it would like to be and the parent decides if that is acceptable. This method often gets called a few times, each time passing in different size requirements, seeing if some compromise can be reached. In the end, though, the child needs to respect to the parent's size requirements.

I always go back to this answer when I need a refresher on how to set up my onMeasure:

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

    int desiredWidth = 100;
    int desiredHeight = 100;

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int width;
    int height;

    //Measure Width
    if (widthMode == MeasureSpec.EXACTLY) {
        //Must be this size
        width = widthSize;
    } else if (widthMode == MeasureSpec.AT_MOST) {
        //Can't be bigger than...
        width = Math.min(desiredWidth, widthSize);
    } else {
        //Be whatever you want
        width = desiredWidth;
    }

    //Measure Height
    if (heightMode == MeasureSpec.EXACTLY) {
        //Must be this size
        height = heightSize;
    } else if (heightMode == MeasureSpec.AT_MOST) {
        //Can't be bigger than...
        height = Math.min(desiredHeight, heightSize);
    } else {
        //Be whatever you want
        height = desiredHeight;
    }

    //MUST CALL THIS
    setMeasuredDimension(width, height);
}

In the example above the desired width and height were just set to some defaults. You could instead calculate them beforehand and set them here using a class member variable.

Using the chosen size

After onMeasure, the size of your view is known. This size may or may not be what you requested, but it is what you have to work with now. Use that size to draw the content on your view in onDraw.

Notes

  • Any time that you make a change to your view that affects the appearance but not the size, then call invalidate(). This will cause onDraw to be called again (but not all of those other previous methods).
  • Any time that you make a change to your view that would affect the size, then call requestLayout(). This will start the process of measuring and drawing all over again from onMeasure. It is usually combined with a call to invalidate().
  • If for some reason you really can't determine an appropriate desired size beforehand, then I suppose you can do as @nmw suggests and just request zero width, zero height. Then request a layout (not just invalidate()) after everything has been loaded. This seems like a bit of a waste though, because you are requiring the entire view hierarchy to be laid out twice in a row.
Share:
15,295

Related videos on Youtube

Simon Zettervall
Author by

Simon Zettervall

Updated on September 15, 2022

Comments

  • Simon Zettervall
    Simon Zettervall over 1 year

    I have made a custom View with onDraw() overridden that draws a bitmap on the canvas. When I specify that I want it wrap_content in the layout file it still fills up the entire screen. onMeasure() says this:

    The base class implementation of measure defaults to the background size, unless a larger size is allowed by the MeasureSpec. Subclasses should override onMeasure(int, int) to provide better measurements of their content.

    Ok cool so I know I need to override onMeasure() and work with MeasureSpec. According to this answer

    UNSPECIFIED means the layout_width or layout_height value was set to wrap_content. You can be whatever size you would like.

    Now I get to my problem, how do I at onMeasure() measure my bitmap that is not created yet and measure/wrap it? I know the other Android views MUST do something because they do not block out the entire screen if set to wrap_content. Thanks in advance!

  • Simon Zettervall
    Simon Zettervall over 11 years
    After some digging around I found out that what you said is right. After the view has been created, I gave it the bitmaps and called requestLayout(); which called my onMeasure and made the view the right size. Thanks!
  • Shirish Herwade
    Shirish Herwade about 9 years
    @nmw I'm having same problem, but didn't get idea reading answer above. Can you please add some more explanation here in comment or as edit to your answer above. It will help me a lot.