63

I'm trying to create a screen with both text and images. I want the images to be laid out like a grid, as shown below, but I want them to have no scroll functionality other that the one provided by the surrounding ScrollView.

An image will best illustrate my question:

alt text

<ScrollView>
    <LinearLayout>
        <ImageView />
        <TextView />
        <GridView />
        <TextView />
    </LinearLayout>
</ScrollView>

What is the best way to make show a grid of a varying number of images, where the grid does not have scroll functionality?

Please note that disabling the scroll functionality for the GridView does not work, as this just disables the scrollbars but does not show all items.

Update: The image below shows what it looks like with scrollbars disabled in the GridView.

alt text

hanspeide
  • 2,819
  • 4
  • 25
  • 33
  • Is the number of images fixed, or can it vary? – PearsonArtPhoto Dec 24 '10 at 01:24
  • Did you set android:layout_height="wrap_content" for the GridView when you tried disabling its scroll functionality? – Squonk Dec 24 '10 at 02:23
  • Pearsonartphoto: The number of images varies. – hanspeide Dec 24 '10 at 08:08
  • MisterSquonk: Yes, that does not work. Also, GridViews inside ScrollViews is not deemed to be kosher from what I read around. E.g. Romain Guy writes: "ListView and GridView are not meant to be embedded within one another (the same is true with ScrollView.) And when I say they are not meant to, it's not supported to do so." I have updated the original post with a screenshot to show how it looks like with scrollbars disable and ndroid:layout_height set to "wrap_content" – hanspeide Dec 24 '10 at 08:18
  • 1
    Romain Guy should have included an addHeader() to the GridView. He didn't, hence, we have to bloody hack it !! – Someone Somewhere Aug 11 '14 at 20:52

7 Answers7

190

Oh boy, yeah, you're gonna have trouble with this one. It drives me nuts that ListViews and GridViews can't be expanded to wrap their children, because we all know that they have more beneficial features in addition to their scrolling and the recycling of their children.

Nonetheless, you can hack around this or create your own layout to suit your needs without too much difficulty. Off the top of my head, I can think of two possibilities:

In my own app I have embedded a ListView within a ScrollView. I have done this by explicitly telling the ListView to be exactly as high as its contents. I do it by changing the layout parameters right inside the ListView.onMeasure() method like so:

public class ExpandableListView extends ListView {

    boolean expanded = false;
    
    public ExpandableListView(Context context, AttributeSet attrs, int defaultStyle) {
        super(context, attrs, defaultStyle);
    }
    
    public boolean isExpanded() {
        return expanded;
    }
    
    public void setExpanded(boolean expanded) {
        this.expanded = expanded;
    }
    
    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // HACK!  TAKE THAT ANDROID!
        if (isExpanded()) {         
            // Calculate entire height by providing a very large height hint.
            // View.MEASURED_SIZE_MASK represents the largest height possible.
            int expandSpec = MeasureSpec.makeMeasureSpec(MEASURED_SIZE_MASK,
                        MeasureSpec.AT_MOST);
            super.onMeasure(widthMeasureSpec, expandSpec);
            
            LayoutParams params = getLayoutParams();
            params.height = getMeasuredHeight();
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }
}

This works because when you give the ListView a mode of AT_MOST, it creates and measures all of its children for you, right inside the onMeasure method (I discovered this by browsing through the source code). Hopefully GridView behaves the same, but if it doesn't, you can still measure all the contents of the GridView yourself. But it would be easier if you could trick the GridView into doing it for you.

Now, you must keep in mind that this solution would completely disable the view recycling that makes GridView so efficient, and all those ImageViews will be in memory even if they're not visible. Same goes with my next solution.

The other possibility is to ditch the GridView and create your own layout. You could extend either AbsoluteLayout or RelativeLayout. For example, if you extend RelativeLayout, you could place each image LEFT_OF the previous one, keeping track of the width of each image until you run out of room on that row, and then start the next row by placing the first image of the new row BELOW the tallest image of the last row. To get the images horizontally centered or in equally-spaced columns you'll have to go through even more pain. Maybe AbsoluteLayout is better. Either way, kind of a pain.

starball
  • 20,030
  • 7
  • 43
  • 238
Neil Traft
  • 18,367
  • 15
  • 63
  • 70
  • 3
    I might also mention that if you want to give the GridView a fixed size, and enable scrolling-within-a-ScrollView, that _is_ possible on Android. But you would have to override `GridView.onTouchEvent()` to call `requestDisallowInterceptTouchEvent(true)` (see the documentation for that magical little method). But if you do that, you may see inconsistencies with scrolling on 2.2 and above, because Romain Guy _really_ doesn't want you to have nested scrolling. ;-) – Neil Traft Dec 27 '10 at 18:35
  • 2
    Nice solution! However, I don't think that you really want to use the name "ExpandableListView" here, as that also is an internal android component. Although it's pretty easy to avoid namespace collisions in Java, it's still a bit confusing. – Nik Reiman Oct 19 '11 at 12:17
  • Also, your code never sets the expanded variable, which means that every time onMeasure() is called, it will try to expand itself. – Nik Reiman Oct 19 '11 at 12:31
  • It was a name that made sense for our implementation. What you're seeing here is an inner class. The outer class sets the `expanded` variable depending on desired behavior. There are reasons to our madness. – Neil Traft Oct 20 '11 at 14:46
  • The problem with the gridview is that it takes the height of the cells in the first column only. otherwise, it works! – njzk2 Dec 01 '11 at 16:39
  • Excellent work Neil ! Adapting it for the GridView has been a breeze ! http://stackoverflow.com/a/8483078/358813 – tacone Dec 13 '11 at 01:00
  • please how did you fixed the issue: "when i launch app with different view then layout automatically scroll at bottom" – haythem souissi Feb 21 '13 at 09:17
  • I don't know what you're referring to, Haythem. – Neil Traft Mar 20 '13 at 11:59
  • Why would you do something like this? Why not use a linear layout instead of the list view, with this hack you cancelling all the advantages a list view has – bogdan Sep 18 '13 at 14:55
  • Wrong, ListView still provides nice selection highlighting, focus traversal, and long-press features that you don't get from a LinearLayout. – Neil Traft Sep 23 '13 at 17:37
  • @ haythem souissi if you have a scrollview then use this to scroll it to top again after setting the height scroll.post(new Runnable() { public void run() { scrollView.scrollTo(0, scroll.getTop()-120); } }); :) – farrukh Feb 24 '14 at 05:26
  • Your answer is working good. but its taking too much space between rows by default how can i redues that space. - Nik Reiman – Android_dev Jun 05 '14 at 05:43
  • @NeilTraft: Are there any solutions which can implement recycling? I have a list with plenty of items, the solution you provided by ExpandableHeight is displaying correctly however it will take long time to be loaded and displayed (my grid items are loaded from web). – VSB Sep 25 '14 at 13:36
  • Well, I haven't properly coded on Android in years and I know the API has changed a lot, however... Simply to measure the full height of the list in order to leave enough space for them in the layout requires you to create _all_ the elements. You'd only be able to avoid this if you calculated the heights yourself and everything had a predetermined size. And then you'd likely need to implement recycling yourself, but I don't remember well how the recycling is implemented, so maybe there's some way to hack around it. – Neil Traft Sep 25 '14 at 17:18
  • This does not work for gridviews, I am receiving a binary xml file line #26 inflate error. – portfoliobuilder Apr 09 '15 at 01:21
  • @NeilTraft this solution works great for os higher than android 5.0 but when i test the following in os below 5.0 or 4.2 i get the following error `java.lang.ClassCastException: android.widget.RelativeLayout$LayoutParams cannot be cast to android.widget.AbsListView$LayoutParams` – silverFoxA Jun 05 '15 at 17:36
  • @NeilTraft i want to scroll view inside Linear Layout and Linear Layout size is fix to inside gridview so it's possible or not ? – Hardik Parmar Jul 15 '15 at 15:46
  • @HardikParmar I would avoid having multiple scrollables inside eachother. If the GridView doesn't scroll then I would turn it into a Relative or Absolute Layout. – Neil Traft Jul 15 '15 at 19:04
  • Friend, you saved my project and release date. Hats off to you. – sid_09 Feb 10 '16 at 10:21
  • Didn't work for me. It did not calculate the correct height. The last element is cropped. – Ajit Singh May 18 '16 at 11:23
  • @NeilTraft nice work! It doesn't work with `dividerHeight` set though... – Maurício Giordano May 16 '17 at 22:00
4

A GridView with header and footer can be used instead of trying to embed GridView in ScrollView. Header and footer can be anything - texts, images, lists, etc. There is an example of GridView with header and footer: https://github.com/SergeyBurish/HFGridView

Sergey Burish
  • 584
  • 6
  • 8
3

You have 2 solutions for this one:

  1. Write your own custom layout. This would be the harder solution (but might be considered the correct one).

  2. Set the real height of your GridView in the code. For example:

  RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) myGridView.getLayoutParams();
  // lp.setMargins(0, 0, 10, 10); // if you have layout margins, you have to set them too
  lp.height = measureRealHeight(...);
  myGridView.setLayoutParams(lp);

The measureRealHeight() method should look something like this (hopefully I got it right):

private int measureRealHeight(...)
{
  final int screenWidth = getWindowManager().getDefaultDisplay().getWidth();
  final double screenDensity = getResources().getDisplayMetrics().density;
  final int paddingLeft = (int) (X * screenDensity + 0.5f); // where X is your desired padding
  final int paddingRight = ...;
  final int horizontalSpacing = (int) (X * screenDensity + 0.5f); // the spacing between your columns
  final int verticalSpacing = ...; // the spacing between your rows
  final int columnWidth = (int) (X * screenDensity + 0.5f);             
  final int columnsCount = (screenWidth - paddingLeft - paddingRight + horizontalSpacing - myGridView.getVerticalScrollbarWidth()) / (columnWidth + horizontalSpacing);
  final int rowsCount = picsCount / columnsCount + (picsCount % columnsCount == 0 ? 0 : 1);

  return columnWidth * rowsCount + verticalSpacing * (rowsCount - 1);
}

The above code should work in Android 1.5+.

Edi
  • 820
  • 9
  • 13
  • 3
    This could get messy if you have items loading that might resize the view, such as images. I am really hating that there's no simple solution to this and a bunch of hacks that don't work reliably. – Artem Russakovskii Jun 16 '11 at 23:55
  • works fine if you know the size of your grid items beforehand (the `X` on the `columnWidth` line) – Mortimer Sep 09 '11 at 11:16
2

Create a non scrollable list view like this:

public class ExpandableListView extends ListView{

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

public ExpandableListView(Context context, AttributeSet attrs, int defaultStyle) {
    super(context, attrs, defaultStyle);
}

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

@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int heightMeasureSpec_custom = MeasureSpec.makeMeasureSpec(
            Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
    super.onMeasure(widthMeasureSpec, heightMeasureSpec_custom);
    ViewGroup.LayoutParams params = getLayoutParams();
    params.height = getMeasuredHeight();
  }
}

In your layout file create an element like this:

<com.example.ExpandableListView
           android:layout_width="match_parent"
           android:layout_height="wrap_content"/>

This should work.

user3282666
  • 640
  • 1
  • 7
  • 21
1

I found a way to give the GridView a fixed size inside ScrollView, and enable scrolling it.

To do so, you would have to implement a new class extending GridView and override onTouchEvent() to call requestDisallowInterceptTouchEvent(true). Thus, the parent view will leave the Grid intercept touch events.

GridViewScrollable.java:

package com.example;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.GridView;

public class GridViewScrollable extends GridView {

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

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

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

    @Override
    public boolean onTouchEvent(MotionEvent ev){
        // Called when a child does not want this parent and its ancestors to intercept touch events.
        requestDisallowInterceptTouchEvent(true);
        return super.onTouchEvent(ev);
    }
}

Add it in your layout with the characteristics you want:

<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:isScrollContainer="true" >

    <com.example.GridViewScrollable
    android:id="@+id/myGVS"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:clickable="true"
    android:numColumns="auto_fit"
    android:columnWidth="100dp"
    android:stretchMode="columnWidth" />

</ScrollView>

And just get it in your activity and set the adapter, for example an ArrayAdapter<>:

GridViewScrollable mGridView = (GridViewScrollable) findViewById(R.id.myGVS);
mGridView.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, new String[]{"one", "two", "three", "four", "five"}));

I hope it helps =)

Lionel T.
  • 1,194
  • 1
  • 13
  • 17
  • above code works for you? coz I am trying but its not working. can you please share sample code here. It will be very helpful to me. – Nitish Patel Mar 04 '15 at 11:01
  • Yes it works for me and i'm using it. What error do you have? Don't forget to define the layout as **ScrollView**. I've edited my response that you can understand better. – Lionel T. Mar 05 '15 at 16:30
  • Please explain more than "didn't work". What kind of Adapter are you using? I extend an ArrayAdapter wich contains an `ArrayList` _(image path)_, decodes and puts into an ImageView. Want to help you guys but i need more details. Maybe you should set the GridView as clickable `android:clickable="true"` – Lionel T. Mar 09 '15 at 16:46
  • I am not getting any error but actually my view is not getting scrolled. I am using ArrayAdapter and passing string[] to it.Can you please your MainActivity with an adapter. – Nitish Patel Mar 11 '15 at 04:53
  • I just put in my answer an example with an _ArrayAdapter<>_ and it works. I don't know what's going badly. Maybe if you put your code i could help better. – Lionel T. Mar 11 '15 at 17:13
  • It is works, but not smooth for scrolling :) inside NestedScrollView – Rudi Wijaya Nov 18 '16 at 03:47
0

Try this

 public static void setGridViewHeightBasedOnChildren(GridView gridView, int columns) {
        ListAdapter listAdapter = gridView.getAdapter();
        if (listAdapter == null)
            return;
        int desiredWidth = View.MeasureSpec.makeMeasureSpec(gridView.getWidth(), View.MeasureSpec.UNSPECIFIED);
        int totalHeight = 0;
        View view = null;
        int rows = listAdapter.getCount() / columns;
        if(listAdapter.getCount() % columns> 0){
            rows++;
        }
        for (int i = 0; i < rows; i++) {
            view = listAdapter.getView(i, view, gridView);
            if (i == 0)
                view.setLayoutParams(new ViewGroup.LayoutParams(desiredWidth, LinearLayout.LayoutParams.WRAP_CONTENT));

            view.measure(desiredWidth, View.MeasureSpec.UNSPECIFIED);
            totalHeight += view.getMeasuredHeight();
        }
        ViewGroup.LayoutParams params = gridView.getLayoutParams();
        params.height = totalHeight + (gridView.getHorizontalSpacing() * rows);
        gridView.setLayoutParams(params);
        gridView.requestLayout();
    }
0

For GridView with other View inside of the save ScrollView to make it all scroll, go to this link: http://www.londatiga.net/it/programming/android/make-android-listview-gridview-expandable-inside-scrollview/#comment-3967742. It is helpful and have saved my time which I just spend 5 minute with this when I have never know about it.

Update:

From the link I have customize an ExpandedGridView:

public class ExpandedGridView extends GridView {
    boolean expanded = false;

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

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

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

    public boolean isExpanded() {
        return expanded;
    }

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // HACK! TAKE THAT ANDROID!
        if (isExpanded()) {
            // Calculate entire height by providing a very large height hint.
            // But do not use the highest 2 bits of this integer; those are
            // reserved for the MeasureSpec mode.
            int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
            super.onMeasure(widthMeasureSpec, expandSpec);
            ViewGroup.LayoutParams params = getLayoutParams();
            params.height = getMeasuredHeight();
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }

    public void setExpanded(boolean expanded) {
        this.expanded = expanded;
    }
}

For your xml change from GridView to the ExpandedGridView which have been customized.

<com.your.package.ExpandedGridView
    android:id="@+id/home_screen_list_goals"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:numColumns="2" />

Usage:

Call it in your activity. If in fragment use contentView.findViewById(...). Which contentView is your whole layout defined.

ExpandedGridView gridView = (ExpandedGridView) findViewById(R.id.home_screen_list_goals);
//set data into grid view
gridView.setAdapter(YOUR_ADAPTER_OBJECT);
gridView.setExpanded(true);
Sarith Nob
  • 337
  • 2
  • 13
  • While the link might be helpful to some users, it's always best to include a short summary into your post, in case the link doesn't work anymore in the future. Then people would only get a 404, which isn't really helpful at all. – waka Aug 27 '17 at 18:49
  • Ok, Thank you! I updated the answer. Please +1 for someone who have been helped by the answer. Thank you! – Sarith Nob Aug 27 '17 at 19:06