Skip to content

Instantly share code, notes, and snippets.

@JakeWharton
Created June 2, 2012 02:14
Show Gist options
  • Save JakeWharton/2856179 to your computer and use it in GitHub Desktop.
Save JakeWharton/2856179 to your computer and use it in GitHub Desktop.
ImageView that respects an aspect ratio applied to a specific measurement.
// Copyright 2012 Square, Inc.
package com.squareup.widgets;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.widget.ImageView;
/** Maintains an aspect ratio based on either width or height. Disabled by default. */
public class AspectRatioImageView extends ImageView {
// NOTE: These must be kept in sync with the AspectRatioImageView attributes in attrs.xml.
public static final int MEASUREMENT_WIDTH = 0;
public static final int MEASUREMENT_HEIGHT = 1;
private static final float DEFAULT_ASPECT_RATIO = 1f;
private static final boolean DEFAULT_ASPECT_RATIO_ENABLED = false;
private static final int DEFAULT_DOMINANT_MEASUREMENT = MEASUREMENT_WIDTH;
private float aspectRatio;
private boolean aspectRatioEnabled;
private int dominantMeasurement;
public AspectRatioImageView(Context context) {
this(context, null);
}
public AspectRatioImageView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AspectRatioImageView);
aspectRatio = a.getFloat(R.styleable.AspectRatioImageView_aspectRatio, DEFAULT_ASPECT_RATIO);
aspectRatioEnabled = a.getBoolean(R.styleable.AspectRatioImageView_aspectRatioEnabled,
DEFAULT_ASPECT_RATIO_ENABLED);
dominantMeasurement = a.getInt(R.styleable.AspectRatioImageView_dominantMeasurement,
DEFAULT_DOMINANT_MEASUREMENT);
a.recycle();
}
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!aspectRatioEnabled) return;
int newWidth;
int newHeight;
switch (dominantMeasurement) {
case MEASUREMENT_WIDTH:
newWidth = getMeasuredWidth();
newHeight = (int) (newWidth * aspectRatio);
break;
case MEASUREMENT_HEIGHT:
newHeight = getMeasuredHeight();
newWidth = (int) (newHeight * aspectRatio);
break;
default:
throw new IllegalStateException("Unknown measurement with ID " + dominantMeasurement);
}
setMeasuredDimension(newWidth, newHeight);
}
/** Get the aspect ratio for this image view. */
public float getAspectRatio() {
return aspectRatio;
}
/** Set the aspect ratio for this image view. This will update the view instantly. */
public void setAspectRatio(float aspectRatio) {
this.aspectRatio = aspectRatio;
if (aspectRatioEnabled) {
requestLayout();
}
}
/** Get whether or not forcing the aspect ratio is enabled. */
public boolean getAspectRatioEnabled() {
return aspectRatioEnabled;
}
/** set whether or not forcing the aspect ratio is enabled. This will re-layout the view. */
public void setAspectRatioEnabled(boolean aspectRatioEnabled) {
this.aspectRatioEnabled = aspectRatioEnabled;
requestLayout();
}
/** Get the dominant measurement for the aspect ratio. */
public int getDominantMeasurement() {
return dominantMeasurement;
}
/**
* Set the dominant measurement for the aspect ratio.
*
* @see #MEASUREMENT_WIDTH
* @see #MEASUREMENT_HEIGHT
*/
public void setDominantMeasurement(int dominantMeasurement) {
if (dominantMeasurement != MEASUREMENT_HEIGHT && dominantMeasurement != MEASUREMENT_WIDTH) {
throw new IllegalArgumentException("Invalid measurement type.");
}
this.dominantMeasurement = dominantMeasurement;
requestLayout();
}
}
<declare-styleable name="AspectRatioImageView">
<attr name="aspectRatio" format="float" />
<attr name="aspectRatioEnabled" format="boolean" />
<attr name="dominantMeasurement">
<enum name="width" value="0"/>
<enum name="height" value="1"/>
</attr>
</declare-styleable>
@kennywyland
Copy link

I think line 48 should be dividing by the aspectRatio, not multiplying. It should be:

newHeight = (int) (newWidth / aspectRatio);

The aspectRatio is (desiredWidth/desiredHeight), so:

aspectRatio = (newWidth/newHeight)

multiply both sides by newHeight...

aspectRatio * newHeight = newWidth

divide both sides by aspectRatio...

newHeight = (newWidth / aspectRatio)

@Servus7
Copy link

Servus7 commented Oct 1, 2015

@kennywyland your right. The following calculation for a 16:9 TV with 1920x1080 resolution makes it more understandable:

aspectRatio = 16/9 = 1.78
newWidth = getMeasuredWidth() = 1920
newHeight = (int) (newWidth * aspectRatio) = (int) (1920 * 1.78) = 3413

But newHeight should be 1080 to fit the aspect ratio.

newHeight = (int) (newWidth / aspectRatio) = (int) (1920 / 1.78) = 1080

In line 48 therefore has to be multiplied rather than divided.

@alexshr
Copy link

alexshr commented Jun 18, 2017

@kennywyland @Servus7
I guess you are not right.

First of all you should choose measurement (dominant) you want to measure (width or height)
Suppose you choose width as dominant.
In this case you can set aspectRatio >1 to make height>width and vice versa

@eugenebrusov
Copy link

With introduction of ConstraintLayout, this solution seems a bit clumsy. Now to keep your ImageView in 16:9 ratio, you simply need to build your ImageView into ConstraintLayout:

<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginEnd="0dp"
        android:layout_marginStart="0dp"
        android:layout_marginTop="0dp"
        app:srcCompat="@mipmap/ic_launcher"
        app:layout_constraintDimensionRatio="16:9"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

Don't forget to add constraint-layout dependency to your module's build.gradle file

implementation "com.android.support.constraint:constraint-layout:1.0.2"

Or you can directly edit your layout in Layout Editor in this way:

22_01

@bastidest
Copy link

Is there a solution for variable height of the parent object with a ConstraintLayout. @eugenebrusov's solution is working just fine for parent views of static height like "match_parent" or "20dp", but when I try to use a parent view with a height of "wrap_content" it is not working because the parent object will have a height of 0. Am I doing something wrong?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment