Creating custom Android views – Part 4: Measuring and how to force a view to be square

Last time we saw the LineChart view (in part 3) it looked like this:

What I want you to notice now is not the appearance of the line chart view itself, but the size and position of it. As you can see, the line chart view has a specific size. How is that size determined? In this case there is a vertical LinearLayout and the LineChartView and the TextView below it share the available height equally using weights on the list. So what would happen if we removed the TextView and all the weights and set the height of the line chart view to wrap_content? Here’s how it looks then:

As you can see, when the height of the LineChartView is set to wrap_content it completely fills the available size. This is the default behaviour of View. But what if we want to change that?

The layout process

If you want to change the default behaviour of View in our custom view, then we need to override onMeasure. Depending on how you use your custom view and what it is you probably want to override onMeasure, at least if you, like in this case, extend View directly and not one of the subclasses.

Measuring the view is part of the layouting process. The layout process consists of two passes, measuring and layouting. A bit simplified we can say that the measure pass sets how big the view should be, the dimensions of it, and that the layout pass sets where to place the view, the position of it. The layout part is only interesting for views with children, in other words views that inherits from ViewGroup. For our LineChartView, which is a simple View, the layout part is neither interesting nor applicable. The measuring part however is definitely interesting.

Squaring the view

To modify the measuring we override the onMeasure() method. Here you get two integers, one for the width and one for the height. These integers are a combination of the width and height that the parent suggests and a mode that tells you how to use the suggested size. The class MeasureSpec is used to extract the size and the mode of the integers. There are three modes. They are UNSPECIFIED, AT_MOST and EXACTLY and they mean exactly what they are called. So if the size is 300 and the mode is AT_MOST, then the parent says that it’s up to you to decide the size, but you can be at most 300 pixels.

One quite common goal is to make a view square. So, let’s override onMeasure() to make the view square.

There is nothing very complicated here. The first thing that happen is that we call the superclass implementation of onMeasure. The reason for that is that measuring can be quite a complex process and calling the super method is a convenient way to get most of this complexity handled.

Then we get the measured width and height of the view. We can’t use getWidth() or getHight() here. During the measuring pass the view has not gotten its final size yet (this happens first at the start of the layout pass) so we have to use getMeasuredWidth() and getMeasuredHeight().

Finally we have some simple logic that calculates the size of the view and calls setMeasuredDimension() to set that size.

If you override onMeasure() you have to call setMeasuredDimension(). This is how you report back the measured size.  If you don’t call setMeasuredDimension() the parent will throw an exception and your application will crash. We are calling the onMeasure() method of the superclass so we don’t actually need to call setMeasuredDimension() since that takes care of that. However, the purpose with overriding onMeasure() was to change the default behaviour and to do that we need to call setMeasuredDimension() with our own values.

As you can see, the view is now square. However, there are a few things that we forgot about in our onMeasure(). One thing is padding. Right now our view is square, but this includes the padding. If we have the same padding on all sides, this doesn’t really matter, but if not, then the actual content in the view will not be square and that is most likely what you want. So we need to update our onMeasure() to take padding into account.

The difference here is that before we compare the width and height of the view, we remove the padding, and when we set the dimension we add it back again. Now the actual content of the view will be square, but, depending on the padding, the total dimensions of the view might not be.

Another thing that we have forgotten about is the modes. Since we call the onMeasure() of the superclass most of this is handled for us, but since we do modify the dimensions we actually break some of the contract between view parent and view child. I’m not going to handle that here though since it’s not really necessary.

Supporting any ratio

A square view can be convenient in many places. For example, extending ImageView and adding the above onMeasure() you have an image view that will always be square. But now when we’ve gotten this far, why not make it a bit more general and support any ratio, not just 1:1.

With this code, we can set what ever ratio we want, in this case 4:3. As before, it doesn’t always obey the parent since we disregard the EXACTLY mode. But if we would do that, we could of course not guarantee that the view has the specified ratio.

In the next part, we’ll take a look at how measuring and layouting works when extending a ViewGroup instead.

You can find the sources to this tutorial here.

This Post Has 20 Comments

  1. This is an excellent tutorial !

    With new Android devices having multiple cores, it is very beneficial to perform calculations from another thread instead of the UI (main) thread. Is it possible to perform the dynamics calculations within another thread ?

    With the tutorial as-is, the UI thread is used to make these calculations. It would be better to run that in a separate thread (hopefully separate core).

    Thanks !

    1. Thanks Alex! Glad you liked it.

      It’s certainly possible to have the dynamic calculations in another tread, but in this case I don’t think it would speed it up any. At least not enough to warrant all the complexity a new thread brings.

      1. Thanks for your fast response – I was thinking the additional threads would come in handy when drawing multiple lines on the same chart

  2. Iluminating!! Thank you.
    Looking foreward to the next part!

  3. Please send me url for measuring and layouting works when extending a ViewGroup tutorila

  4. Tutorial is very helpful. I only run into the following:
    When I create a custom view and use your onMeasure method and draw a rectangle in the onDraw it’s a square. But when I give the view a background color and change the orientation of the device to landscape the background is bigger then the square. Can you explain that?

  5. How to make x and y axis scrollable?

  6. creating scrollable x and y to the view with constant spacing between points.

    Thank you

    1. Making it scrollable can be quite complicated depending on your requirements, but an easy way is to simply make it larger and put it inside a ScrollView.

  7. Thanks for the tutorial. I am also interested in the different considerations for ViewGroups as was mentioned at the end.

  8. Thanks, helped me a lot.

  9. Thanks for grate tutorials!
    I’m just wondering if you are planing to cover scroll and pinch gestures in this series ?

  10. Thanks a lot really useful! Did my job! :)

  11. How can we put x axis and y axis with labels of respective metrics like aChartEngine ? Please help me out.

  12. Thanks for a great article.

    Any plan on writing the followup tutorial to this on how measuring and layouting works when extending a ViewGroup?

  13. Thank you so much for your great post.
    And I have one question about your code in LineChartView Class.
    I think we should add one line to the method, setChartData() like below.

    else {
    for (int i = 0; i < newDatapoints.length; i++) {
    datapoints[i].setPosition(datapoints[i].getPosition(), now); <=== like this.
    datapoints[i].setTargetPosition(newDatapoints[i], now);

    We need to change the "point" with the older "Target point".
    Am I wrong? :)

  14. The greater than sign (>) in your code blocks is displaying as “>”.

  15. private static final float RATIO = 4f / 3f;
    If you want to make width:height = 4:3, then what do the following lines mean ?

    int maxWidth = (int) (heigthWithoutPadding * RATIO);
    int maxHeight = (int) (widthWithoutPadding / RATIO);

    Shouldn’t we just pick either heigthWithoutPadding or widthWithoutPadding and then multiply it by 4 for the maxWidth and by 3 for the maxHeight ?

  16. Thank you for the clear & helpful article.

  17. wtf is this
    if (widthWithoutPadding > maxWidth) {

Leave a Reply

Close Menu