mattias | niklewski.com

Fixed Aspect Layout

ImageView.ScaleType offers a flexible way to scale and align images without changing their aspect ratio. One day I was building a side-bar whose shape would vary depending on screen size, and I thought "hey, it would be nice if I could have the same flexibility, not with images but with layouts". So I wrote this simple custom layout which has been a valuable asset in my toolkit ever since.

public class FixedAspectLayout extends FrameLayout {

    private float aspect = 1.0f;

    // .. alternative constructors omitted

    public FixedAspectLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.FixedAspectLayout);
        aspect = a.getFloat(R.styleable.FixedAspectLayout_aspectRatio, 1.0f);
    }

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

        if (w == 0) {
            h = 0;
        } else if (h / w < aspect) {
            w = (int)(h / aspect);
        } else {
            h = (int)(w * aspect);
        }

        super.onMeasure(
                MeasureSpec.makeMeasureSpec(w,
                        MeasureSpec.getMode(widthMeasureSpec)),
                MeasureSpec.makeMeasureSpec(h,
                        MeasureSpec.getMode(heightMeasureSpec)));
    }
}

This version of the code has a bugfix that I borrowed from user TalL's answer on SO, which solves the exact same problem. Thanks!

The code in init() relies on a new styleable that you define by adding the following resource to res/values/attrs.xml.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="FixedAspectLayout">
        <attr name="aspectRatio" format="float"/>
    </declare-styleable>
</resources>

Custom styleables are defined in an app-specific namespace which you need to import. Here is an example of how to do that. Replace com.example.layouttest with the package name of your app.

xmlns:app="http://schemas.android.com/apk/res/com.example.layouttest"

...

<com.example.layouttest.FixedAspectLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    app:aspectRatio="1.0" >

    ... child layout ...

</com.example.layouttest.FixedAspectLayout>

The child layout can be a single view or a complex layout. The FixedAspectLayout itself can be positioned as any other view, so you might for instance use a gravity to center it within its parent. Keep in mind that if your layout is dynamic like mine was, you may not know in advance if there is going to be excess horizontal or vertical space, so you may want to specify both a horizontal and vertical alignment.

Here is a screenshot that shows the layout in action with various values for app:aspect (a) and android:gravity (g). Note how the aspect ratio (not the size) stays constant despite the varying shape of the container.