Introduction

A lot of the custom Views I have made all have to respect some aspect ratio. In order to make the measuring of these Views easier (and more modular, to avoid that horrible copy-pasting of code), I have created a helper class that will help you measure your custom Views that respect an aspect ratio.

The solution

Behold: The ViewAspectRatioMeasurer ! (Available for download in the GitHub repository: ViewAspectRatioMeasurer.java. You can also find the code in the bottom of this post)

How to use

Simply instantiate the ViewAspectRatioMeasurer when your custom View is constructed, and run the measure() method in your onMeasure() method:

public class MyView extends View { public MyView(Context context) { super(context); } // The aspect ratio to be respected by the measurer private static final double VIEW_ASPECT_RATIO = 2.5; private ViewAspectRatioMeasurer varm = new ViewAspectRatioMeasurer( VIEW_ASPECT_RATIO ); @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { varm.measure(widthMeasureSpec, heightMeasureSpec); setMeasuredDimension( varm.getMeasuredWidth(), varm.getMeasuredHeight() ); } }

How it works

When measuring, there are four possible options in regards to whether the height and width are fixed or dynamic. Dynamic being using "wrap_content" as size, and fixed is "match_parent" or an explicit dimension (e.g. "100dp" ). These four options are:

Both width and height fixed: Fixed (Aspect ratio isn’t respected) Width dynamic, height fixed: Set width depending on height Width fixed, height dynamic: Set height depending on width Both width and height dynamic: Largest size possible

All of these options (except the first) respect the provided aspect ratio.

The code

For the lazy, I have included the source code below:

/** * This class is a helper to measure views that require a specific aspect ratio.<br /> * <br /> * The measurement calculation is differing depending on whether the height and width * are fixed (match_parent or a dimension) or not (wrap_content) * * <pre> * | Width fixed | Width dynamic | * ---------------+-------------+---------------| * Height fixed | 1 | 2 | * ---------------+-------------+---------------| * Height dynamic | 3 | 4 | * </pre> * Everything is measured according to a specific aspect ratio.<br /> * <br /> * <ul> * <li>1: Both width and height fixed: Fixed (Aspect ratio isn't respected)</li> * <li>2: Width dynamic, height fixed: Set width depending on height</li> * <li>3: Width fixed, height dynamic: Set height depending on width</li> * <li>4: Both width and height dynamic: Largest size possible</li> * </ul> * * @author Jesper Borgstrup */ public class ViewAspectRatioMeasurer { private double aspectRatio; /** * Create a ViewAspectRatioMeasurer instance.<br/> * <br/> * Note: Don't construct a new instance everytime your <tt>View.onMeasure()</tt> method * is called.<br /> * Instead, create one instance when your <tt>View</tt> is constructed, and * use this instance's <tt>measure()</tt> methods in the <tt>onMeasure()</tt> method. * @param aspectRatio */ public ViewAspectRatioMeasurer(double aspectRatio) { this.aspectRatio = aspectRatio; } /** * Measure with the aspect ratio given at construction.<br /> * <br /> * After measuring, get the width and height with the {@link #getMeasuredWidth()} * and {@link #getMeasuredHeight()} methods, respectively. * @param widthMeasureSpec The width <tt>MeasureSpec</tt> passed in your <tt>View.onMeasure()</tt> method * @param heightMeasureSpec The height <tt>MeasureSpec</tt> passed in your <tt>View.onMeasure()</tt> method */ public void measure(int widthMeasureSpec, int heightMeasureSpec) { measure(widthMeasureSpec, heightMeasureSpec, this.aspectRatio); } /** * Measure with a specific aspect ratio<br /> * <br /> * After measuring, get the width and height with the {@link #getMeasuredWidth()} * and {@link #getMeasuredHeight()} methods, respectively. * @param widthMeasureSpec The width <tt>MeasureSpec</tt> passed in your <tt>View.onMeasure()</tt> method * @param heightMeasureSpec The height <tt>MeasureSpec</tt> passed in your <tt>View.onMeasure()</tt> method * @param aspectRatio The aspect ratio to calculate measurements in respect to */ public void measure(int widthMeasureSpec, int heightMeasureSpec, double aspectRatio) { int widthMode = MeasureSpec.getMode( widthMeasureSpec ); int widthSize = widthMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : MeasureSpec.getSize( widthMeasureSpec ); int heightMode = MeasureSpec.getMode( heightMeasureSpec ); int heightSize = heightMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : MeasureSpec.getSize( heightMeasureSpec ); if ( heightMode == MeasureSpec.EXACTLY && widthMode == MeasureSpec.EXACTLY ) { /* * Possibility 1: Both width and height fixed */ measuredWidth = widthSize; measuredHeight = heightSize; } else if ( heightMode == MeasureSpec.EXACTLY ) { /* * Possibility 2: Width dynamic, height fixed */ measuredWidth = (int) Math.min( widthSize, heightSize * aspectRatio ); measuredHeight = (int) (measuredWidth / aspectRatio); } else if ( widthMode == MeasureSpec.EXACTLY ) { /* * Possibility 3: Width fixed, height dynamic */ measuredHeight = (int) Math.min( heightSize, widthSize / aspectRatio ); measuredWidth = (int) (measuredHeight * aspectRatio); } else { /* * Possibility 4: Both width and height dynamic */ if ( widthSize > heightSize * aspectRatio ) { measuredHeight = heightSize; measuredWidth = (int)( measuredHeight * aspectRatio ); } else { measuredWidth = widthSize; measuredHeight = (int) (measuredWidth / aspectRatio); } } } private Integer measuredWidth = null; /** * Get the width measured in the latest call to <tt>measure()</tt>. */ public int getMeasuredWidth() { if ( measuredWidth == null ) { throw new IllegalStateException( "You need to run measure() before trying to get measured dimensions" ); } return measuredWidth; } private Integer measuredHeight = null; /** * Get the height measured in the latest call to <tt>measure()</tt>. */ public int getMeasuredHeight() { if ( measuredHeight == null ) { throw new IllegalStateException( "You need to run measure() before trying to get measured dimensions" ); } return measuredHeight; } }