How to layout a 'grid' of images in the center of the screen
Asked Answered
S

1

6

I have a set of 12 images to show in a layout. The number is chosen semi arbitrarily, but boils down to 3 horizontaly and 4 vertically for portrait mode and the other way around for landscape.

First implementation was using a gridview. Problem is the height cannot be forced to fit in the screen. For a workaround I can (try to) scale the images ofcourse, but its near impossible to calculate the room available to the gridview: Total screensize is known ofcourse, but you have to 'guess' the size of the notification bar and that doesn't seem to be an elegant sollution. The measured size isn't really to be trusted: I'm not doing a complete restart on orientation change (for speed), but the way the screen is build the complete space isn't available on the spot. In the end the conclusion is that I don't want to calculate the size of the images and then scale them accordingly: I think it's better to say how the views should fit in the screen, right?

So the next try is just using a TableLayout. Using "ShrinkColumns="*" the images fitted fine, so the size of the images is now how we want them. But 'extra' room we might have in the height is now evenly divided between the tablerows. This is to be expected, but ugly.

The current code seems irrelevant because it doesn’t work, but looks like this in the end: I’ve removed all the padding and other stuff which doesnt seem relevant. (for portait:)

<TableLayout
       android:shrinkColumns="*">
    <TableRow>
        <ImageView/>
        <ImageView/>
        <ImageView/>
    </TableRow>
    … (repeat 3 tablerows)
</TableLayout>

To ‘scale’ the too-large images, the TableLayout has the “shrinkcolumns=”*”” attribute.

How can we get the three ImageViews to be aligned in the center of the TableRow and not spread evenly in the width? And the same goes for the vertical columns, how can we keep everything together and not spread across the height of the screen? Basically, the "excess" space should go to the sides as padding/margin, and now it goes in between the Images.

Example: The left screen-shot shows too much left/right distance, the right has too much top/bottom example-androblip-layout-image

Sherrill answered 4/1, 2011 at 9:17 Comment(4)
What exactly should cause this "excess" space? Do you want the ImageViews to be a fixed size (in dp)? Or a fixed aspect ratio?Gooseflesh
The images are square, so there is not an even division possible. I don't want to calculate how to scale the images for every screen, so i'm trying to fit them horizontally and vertically. Different padding/margin between the images is very ugly (for instance 3 pixels left/right, but 10 pixels top/bottom.). Does that make it any clearer? :)Sherrill
Square images, as large as possible, with a fixed margin between them. Gotcha. It's the "square but scalable" bit that makes this difficult, I think...Gooseflesh
Added a quick screenshot for clarificationSherrill
G
26

I figured this shouldn't be too hard using a custom view, which should be an interesting exercise. This is my first custom view; feedback is welcome!

Limitations

  • AspectGrid completely ignores the size that its children want to be. For your purposes, this appears to be okay. If something fancier is needed, onMeasure needs a lot of extra work.
  • The size that is suggested by AspectGrid's parent is used without a second thought. This is related to the previous issue.

Screenshots

Landscape screenshot http://tinypic.com/images/404.gif

Portrait screenshot

How it works

A main parameter is the number of columns. The number of rows is computed automatically, because we know the number of children. Another main parameter is the aspect ratio we want to use for the children (set to 1 for squares).

In onLayout, we receive the final size of the grid, so we can compute the maximum width and height of the children.

We then check this against the aspect ratio. If the children are too tall, we make them shorter (as in the portrait example). If they are too wide, we make them narrower (as in the landscape example).

That's all there is to it; the rest is just plumbing.

The code

com/photogrid/AspectGrid.java: The actual ViewGroup class

package com.photogrid;

import android.content.Context;

public class AspectGrid extends ViewGroup {

    private int mNumColumns = 1;
    private int mHorizontalSpacing = 0;
    private int mVerticalSpacing = 0;
    private float mChildAspectRatio = 1.0f;

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

    public AspectGrid(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

        try {
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AspectGrid);

            setNumColumns(a.getInt(R.styleable.AspectGrid_numColumns, mNumColumns));
            setHorizontalSpacing(a.getDimensionPixelSize(R.styleable.AspectGrid_horizontalSpacing, mHorizontalSpacing));
            setVerticalSpacing(a.getDimensionPixelSize(R.styleable.AspectGrid_verticalSpacing, mVerticalSpacing));
            setChildAspectRatio(a.getFloat(R.styleable.AspectGrid_childAspectRatio, mChildAspectRatio));

            a.recycle();
        } catch (RuntimeException ex) {
            throw ex;
        }
    }

    public int getNumColumns() {
        return mNumColumns;
    }

    public void setNumColumns(int numColumns) {
        if (numColumns < 1)
            throw new IllegalArgumentException("numColumns must be at least 1");
        if (numColumns != mNumColumns) {
            mNumColumns = numColumns;
            requestLayout();
        }
    }

    public int getHorizontalSpacing() {
        return mHorizontalSpacing;
    }

    public void setHorizontalSpacing(int horizontalSpacing) {
        mHorizontalSpacing = horizontalSpacing;
    }

    public int getVerticalSpacing() {
        return mVerticalSpacing;
    }

    public void setVerticalSpacing(int verticalSpacing) {
        mVerticalSpacing = verticalSpacing;
    }

    public float getChildAspectRatio() {
        return mChildAspectRatio;
    }

    public void setChildAspectRatio(float childAspectRatio) {
        if (childAspectRatio <= 0)
            throw new IllegalArgumentException("childAspectRatio must be positive");
        if (childAspectRatio != mChildAspectRatio) {
            mChildAspectRatio = childAspectRatio;
            requestLayout();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int measuredWidth = widthSize;
        int measuredHeight = heightSize;
        int width = Math.max(measuredWidth, getSuggestedMinimumWidth());
        int height = Math.max(measuredHeight, getSuggestedMinimumHeight());
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        if (childCount <= 0)
            return;

        int innerWidth = r - l - getPaddingLeft() - getPaddingRight();
        int innerHeight = b - t - getPaddingBottom() - getPaddingTop();
        int numRows = (childCount + mNumColumns - 1) / mNumColumns;

        int leftEdge = getPaddingLeft();
        int topEdge = getPaddingTop();
        int horizontalStride = (innerWidth + mHorizontalSpacing) / mNumColumns;
        int verticalStride = (innerHeight + mVerticalSpacing) / numRows;
        int childWidth = horizontalStride - mHorizontalSpacing;
        int childHeight = verticalStride - mVerticalSpacing;

        if (childHeight * mChildAspectRatio > childWidth) {
            childHeight = (int)(childWidth / mChildAspectRatio);
            verticalStride = childHeight + mVerticalSpacing;
            topEdge = (innerHeight + mVerticalSpacing - numRows * verticalStride) / 2; 
        } else {
            childWidth = (int)(childHeight * mChildAspectRatio);
            horizontalStride = childHeight + mHorizontalSpacing;
            leftEdge = (innerWidth + mHorizontalSpacing - mNumColumns * horizontalStride) / 2;
        }

        for (int i = 0; i < childCount; ++i) {
            View child = getChildAt(i);
            int row = i / mNumColumns;
            int column = i % mNumColumns;
            int left = leftEdge + column * horizontalStride;
            int top = topEdge + row * verticalStride;
            child.layout(
                left,
                top,
                left + childWidth,
                top + childHeight);
        }
    }

}

res/values/attrs.xml: Declaration of attributes for use in the XML

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="AspectGrid">
        <attr name="numColumns" format="integer"/>
        <attr name="horizontalSpacing" format="dimension"/>
        <attr name="verticalSpacing" format="dimension"/>
        <attr name="childAspectRatio" format="float"/>
    </declare-styleable>
</resources>

res/layout/main.xml: The example used in the screenshots above

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res/com.photogrid"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <com.photogrid.AspectGrid
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:padding="5dp"
        app:numColumns="3"
        app:horizontalSpacing="5dp"
        app:verticalSpacing="5dp"
        app:childAspectRatio="1.0"
        >
        <TextView  
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"
            android:background="#ffcccc" 
            android:text="Item 1"
            />
        <TextView  
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"
            android:background="#ccffcc" 
            android:text="Item 2"
            />
        <TextView  
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"
            android:background="#ccccff" 
            android:text="Item 3"
            />
        <TextView  
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"
            android:background="#ffffcc" 
            android:text="Item 4"
            />
        <TextView  
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"
            android:background="#ffccff" 
            android:text="Item 5"
            />
        <TextView  
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"
            android:background="#ccffff" 
            android:text="Item 6"
            />
    </com.photogrid.AspectGrid>
</LinearLayout>
Gooseflesh answered 5/1, 2011 at 23:41 Comment(4)
Dude! Magic seems to happen in the onLayout and onMeasure: The problems I had with measured size being a pain (especially after rotation) might be fixed with these methods? Mostly the trouble was the parent (a top / side bar) had to be drawn, and wasn't always on time for some reason. I'll try to use this today and see what happens! ;DSherrill
Working perfectly! You have gone above and beyond the call of duty, actually providing a complete working example :DSherrill
@thomas pleas can you help for this #13416920 ?Dunghill
@thomas it is working perfectly.. can you pls tell me how to align the text in the middle of the box. I tried using "android:gravity='center'" but it is not actually aligning exactly at the middle of the box. Any pointers?Tahr

© 2022 - 2024 — McMap. All rights reserved.