Fixed aspect ratio View

53,386

Solution 1

I implemented FixedAspectRatioFrameLayout, so I can reuse it and have any hosted view be with fixed aspect ratio:

public class FixedAspectRatioFrameLayout extends FrameLayout
{
    private int mAspectRatioWidth;
    private int mAspectRatioHeight;

    public FixedAspectRatioFrameLayout(Context context)
    {
        super(context);
    }

    public FixedAspectRatioFrameLayout(Context context, AttributeSet attrs)
    {
        super(context, attrs);

        init(context, attrs);
    }

    public FixedAspectRatioFrameLayout(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);

        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs)
    {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FixedAspectRatioFrameLayout);

        mAspectRatioWidth = a.getInt(R.styleable.FixedAspectRatioFrameLayout_aspectRatioWidth, 4);
        mAspectRatioHeight = a.getInt(R.styleable.FixedAspectRatioFrameLayout_aspectRatioHeight, 3);

        a.recycle();
    }
    // **overrides**

    @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec)
    {
        int originalWidth = MeasureSpec.getSize(widthMeasureSpec);

        int originalHeight = MeasureSpec.getSize(heightMeasureSpec);

        int calculatedHeight = originalWidth * mAspectRatioHeight / mAspectRatioWidth;

        int finalWidth, finalHeight;

        if (calculatedHeight > originalHeight)
        {
            finalWidth = originalHeight * mAspectRatioWidth / mAspectRatioHeight; 
            finalHeight = originalHeight;
        }
        else
        {
            finalWidth = originalWidth;
            finalHeight = calculatedHeight;
        }

        super.onMeasure(
                MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY), 
                MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY));
    }
}

Solution 2

For new users, here's a better non-code solution :

A new support library called Percent Support Library is available in Android SDK v22 (MinAPI is 7 me thinks, not sure) :

src : android-developers.blogspot.in

The Percent Support Library provides percentage based dimensions and margins and, new to this release, the ability to set a custom aspect ratio via app:aspectRatio. By setting only a single width or height and using aspectRatio, the PercentFrameLayout or PercentRelativeLayout will automatically adjust the other dimension so that the layout uses a set aspect ratio.

To include add this to your build.gradle :

compile 'com.android.support:percent:23.1.1'

Now wrap your view (the one that needs to be square) with a PercentRelativeLayout / PercentFrameLayout :

<android.support.percent.PercentRelativeLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content">
     <ImageView
         app:layout_aspectRatio="100%"
         app:layout_widthPercent="100%"/>
 </android.support.percent.PercentRelativeLayout>

You can see an example here.

Solution 3

To not use third-party solution and considering the fact that both PercentFrameLayout and PercentRelativeLayout were deprecated in 26.0.0, I'd suggest you to consider using ConstraintLayout as a root layout for your grid items.

Your item_grid.xml might look like:

<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="wrap_content">

    <ImageView
        android:id="@+id/imageview_item"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:scaleType="centerCrop"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintDimensionRatio="H,1:1" />

</android.support.constraint.ConstraintLayout>

As a result you get something like this:

Fixed ratio sized grid items

Solution 4

I recently made a helper class for this very problem and wrote a blog post about it.

The meat of the code is as follows:

/**
 * Measure with a specific aspect ratio<br />
 * <br />
 * @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);
        }

    }
}

Solution 5

I created a layout library using TalL's answer. Feel free to use it.

RatioLayouts

Installation

Add this to the top of the file

repositories {
    maven {
        url  "http://dl.bintray.com/riteshakya037/maven" 
    }
}


dependencies {
    compile 'com.ritesh:ratiolayout:1.0.0'
}

Usage

Define 'app' namespace on root view in your layout

xmlns:app="http://schemas.android.com/apk/res-auto"

Include this library in your layout

<com.ritesh.ratiolayout.RatioRelativeLayout
        android:id="@+id/activity_main_ratio_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:fixed_attribute="WIDTH" // Fix one side of the layout 
        app:horizontal_ratio="2" // ratio of 2:3
        app:vertical_ratio="3">

Update

With introduction of ConstraintLayout you don't have to write either a single line of code or use third-parties or rely on PercentFrameLayout which were deprecated in 26.0.0.

Here's the example of how to keep 1:1 aspect ratio for your layout using ConstraintLayout:

<android.support.constraint.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginEnd="0dp"
        android:layout_marginStart="0dp"
        android:layout_marginTop="0dp"
        android:background="@android:color/black"
        app:layout_constraintDimensionRatio="H,1:1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

    </LinearLayout>

</android.support.constraint.ConstraintLayout>
Share:
53,386
Jani
Author by

Jani

Updated on August 26, 2020

Comments

  • Jani
    Jani over 3 years

    How would I go implementing a fixed aspect ratio View? I'd like to have items with 1:1 aspect ratio in a GridView. I think it's better to subclass the children than the GridView?

    EDIT: I assume this needs to be done programmatically, that's no problem. Also, I don't want to limit the size, only the aspect ratio.

  • Zsolt Safrany
    Zsolt Safrany over 11 years
    The Init method should start with a lower-case. Java convention.
  • CommonsWare
    CommonsWare about 11 years
    I just implemented something along these lines, based on PreviewFrameLayout from the Camera AOSP app. However, I am noticing that gravity no longer seems to be honored -- in cases where the height is constrained, all the extra whitespace seems to be at the bottom even with Gravity.CENTER or Gravity.BOTTOM, for example. Have you seen this? If so, did you happen to come up with a solution? Thanks!
  • Oliv
    Oliv about 11 years
    @CommonsWare put the FixedAspectRatioFrameLayout inside a RelativeLayout and add android:layout_centerInParent="true" to the FixedAspectRatioFrameLayout
  • CommonsWare
    CommonsWare about 11 years
    @Oliv: Yeah, I wound up doing something along those lines: github.com/commonsguy/cwac-layouts
  • aleb
    aleb almost 11 years
    A FixedAspectRatioFrameLayout based on this code: github.com/triposo/barone/blob/master/src/com/triposo/barone‌​/…
  • Prakash Nadar
    Prakash Nadar almost 10 years
    I am not sure if you have to do all these things, just call the baseclass to onMeasure to do the heavylifting and fix the measuredheight from the measuredwidth * aspect ratio before returning.
  • JesperB
    JesperB almost 10 years
    Depends on what you want to do. I think you are describing possibility 3 (width fixed, height dynamic) in the code above.
  • tar
    tar almost 10 years
    Paste this into a res/values/attrs.xml: ` <declare-styleable name="FixedAspectRatioFrameLayout"> <attr name="aspectRatioWidth" format="integer" /> <attr name="aspectRatioHeight" format="integer" /> </declare-styleable> `
  • Gallal
    Gallal over 8 years
    @tar - you need to wrap <declare-styleable ...> in <resources></resources>
  • TalL
    TalL almost 8 years
    Implement this class in a new file, and then use it in a layout instead of a regular FrameLayout. Google "implementing custom views in Android" if you need the basics. But I would suggest the solution below of using googles own percent support library (which didn't exist when I wrote this solution).
  • GuillermoMP
    GuillermoMP over 7 years
    While PercentRelativeLayout is good for some use cases, it doesn't work with scroll views pre M and is way more complex since it supports child sizing. If you just need a fixed aspect ratio view then accepted answer is just best.
  • JediBurrell
    JediBurrell almost 7 years
    The percent library is deprecated as of 26-rc1 in favor of ConstraintLayout... Which isn't very related to one another.
  • GenError
    GenError over 4 years
    Thanks, this works like a charm, however, Android Studio complains that "H,1:1" is an invalid float. If you set app:layout_constraintDimensionRatio="1.0" it works the same way but without the warning.