Fixed aspect ratio View
Asked Answered
B

10

57

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.

Bainbridge answered 14/8, 2011 at 17:34 Comment(4)
Jesper Borgstrup has created a helper class to use with onMeasure: buzzingandroid.com/2012/11/…Hygeia
Google has provided the Percent Support Library: developer.android.com/tools/support-library/…Hygeia
@Hygeia Classes PercentFrameLayout and PercentRelativeLayout from Support Library were deprecated in 26.0.0. For now you should consider using ConstraintLayout to size your views in predefined aspect ratio. See my answer below.Muster
This is a JQuery plugin that helps with aspect ratio: github.com/eissasoubhi/aspectratio.jsBlowy
H
85

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));
    }
}
Harding answered 27/5, 2012 at 8:11 Comment(7)
The Init method should start with a lower-case. Java convention.Milena
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!Bindle
@Bindle put the FixedAspectRatioFrameLayout inside a RelativeLayout and add android:layout_centerInParent="true" to the FixedAspectRatioFrameLayoutDoukhobor
@Oliv: Yeah, I wound up doing something along those lines: github.com/commonsguy/cwac-layoutsBindle
Paste this into a res/values/attrs.xml: ` <declare-styleable name="FixedAspectRatioFrameLayout"> <attr name="aspectRatioWidth" format="integer" /> <attr name="aspectRatioHeight" format="integer" /> </declare-styleable> `Brodie
@Brodie - you need to wrap <declare-styleable ...> in <resources></resources>Hospitality
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).Harding
M
40

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

Muster answered 25/9, 2017 at 15:40 Comment(1)
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.Legionnaire
T
38

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.

Tila answered 15/2, 2016 at 22:14 Comment(2)
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.Duello
The percent library is deprecated as of 26-rc1 in favor of ConstraintLayout... Which isn't very related to one another.Priester
P
9

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

    }
}
Purdum answered 12/12, 2012 at 18:53 Comment(3)
A FixedAspectRatioFrameLayout based on this code: github.com/triposo/barone/blob/master/src/com/triposo/barone/…Elevenses
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.Adder
Depends on what you want to do. I think you are describing possibility 3 (width fixed, height dynamic) in the code above.Purdum
T
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>
Tasia answered 30/5, 2017 at 6:46 Comment(0)
C
4

I've used and liked Jake Wharton's implementation of ImageView (should go similarly for other views), others might enjoy it too:

AspectRatioImageView.java - ImageView that respects an aspect ratio applied to a specific measurement

Nice thing it's styleable in xml already.

Clynes answered 24/8, 2014 at 20:16 Comment(0)
C
4

You may find third-party libraries. Instead of using them, use constraint layout. Below code sets the aspect ratio of ImageView as 16:9 regardless of the screen size and orientation.

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

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:scaleType="fitXY"
        android:src="@drawable/mat3"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.0"
        app:layout_constraintDimensionRatio="H,16:9"/>

</androidx.constraintlayout.widget.ConstraintLayout>

app:layout_constraintDimensionRatio="H,16:9". Here, height is set with respect to the width of the layout.

Portrait activity

Landscape activity

For your question, set android:layout_width="match_parent" and use app:layout_constraintDimensionRatio="H,1:1" in your view.

Cannon answered 25/8, 2020 at 17:6 Comment(0)
L
3

Simply override onSizeChanged and calculate ratio there.

Formula for aspect ratio is:

newHeight =  original_height / original_width x new_width

this would give you something like that:

 @Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);

    //3:5 ratio
    float RATIO = 5/3;
    setLayoutParams(new LayoutParams((int)RATIO * w, w));

}

hope this helps!

Lacerta answered 13/2, 2013 at 13:41 Comment(0)
D
2

The ExoPlayer from Google comes with an AspectRatioFrameLayout that you use like this:

<com.google.android.exoplayer2.ui.AspectRatioFrameLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:resize_mode="fixed_width">
    <!-- https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.html#RESIZE_MODE_FIXED_WIDTH -->

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</com.google.android.exoplayer2.ui.AspectRatioFrameLayout>

Then you must set the aspect ratio programmatically:

aspectRatioFrameLayout.setAspectRatio(16f/9f)

Note that you can also set the resize mode programmatically with setResizeMode.


Since you are obviously not going to grab the whole ExoPlayer library for this single class, you can simply copy-paste the file from GitHub to your project (it's open source):

https://github.com/google/ExoPlayer/blob/release-v2/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java

Don't forget to grab the attribute resize_mode too:

https://github.com/google/ExoPlayer/blob/release-v2/library/ui/src/main/res/values/attrs.xml#L18-L25

<attr name="resize_mode" format="enum">
  <enum name="fit" value="0"/>
  <enum name="fixed_width" value="1"/>
  <enum name="fixed_height" value="2"/>
  <enum name="fill" value="3"/>
  <enum name="zoom" value="4"/>
</attr>
Department answered 27/5, 2019 at 10:47 Comment(0)
J
2

I have used in this way.

<androidx.constraintlayout.widget.ConstraintLayout

                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">
                    <ImageView
                        android:id="@+id/imageView_home_one"
                        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,4:3"
                        android:layout_marginTop="@dimen/_5sdp"
                        android:visibility="gone"
                        tools:ignore="MissingConstraints" />
                </androidx.constraintlayout.widget.ConstraintLayout>
Judo answered 24/5, 2022 at 6:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.