Easy measuring of custom Views with specific aspect ratio

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;
	}
	
}

About the author

Jesper Borgstrup Jesper is a Masters student of computer science at the University of Copenhagen with many years of experience in writing applications for Android.

7 thoughts on “Easy measuring of custom Views with specific aspect ratio

    1. Jesper Borgstrup Post author

      Here is your license: Use it however you want. I am not responsible for its use in any ways. :-)

      Reply
  1. Jamil

    Hi, Your post seem to be very helpful, yet because I am new to Java, I cant seem to figure out how to use this class an EditTextView, can you give me any tips on how to implement it.

    Thanks

    Jamil

    Reply
  2. kjsolo

    When I use it on RelativeLayout and make other view in it with centerInParent like this:
    No child view (when I delete super.onMeasure) or child view is not center in parent. I don’t know why.

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (varm != null) {
            varm.measure(widthMeasureSpec, heightMeasureSpec);
            setMeasuredDimension(varm.getMeasuredWidth(), varm.getMeasuredHeight());
        }
    }

    Reply
    1. kjsolo

      I found a solution

      if (varm != null) {
          varm.measure(widthMeasureSpec, heightMeasureSpec);
          int w = varm.getMeasuredWidth();
          int h = varm.getMeasuredHeight();
          super.onMeasure(
                  MeasureSpec.makeMeasureSpec(w, MeasureSpec.getMode(MeasureSpec.EXACTLY)),
                  MeasureSpec.makeMeasureSpec(h, MeasureSpec.getMode(MeasureSpec.EXACTLY)));
      } else {
          super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      }

      Reply
  3. Pingback: 안드로이드 커스텀뷰 이해하기 – Burt K.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>