Grid of images inside ScrollView

51,702

Solution 1

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.

Good luck.

Solution 2

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

Solution 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+.

Solution 4

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.

Solution 5

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 =)

Share:
51,702
hanspeide
Author by

hanspeide

Updated on August 13, 2020

Comments

  • hanspeide
    hanspeide over 3 years

    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

  • Neil Traft
    Neil Traft over 13 years
    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. ;-)
  • Artem Russakovskii
    Artem Russakovskii almost 13 years
    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.
  • Mortimer
    Mortimer over 12 years
    works fine if you know the size of your grid items beforehand (the X on the columnWidth line)
  • Nik Reiman
    Nik Reiman over 12 years
    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
    Nik Reiman over 12 years
    Also, your code never sets the expanded variable, which means that every time onMeasure() is called, it will try to expand itself.
  • Neil Traft
    Neil Traft over 12 years
    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.
  • njzk2
    njzk2 over 12 years
    The problem with the gridview is that it takes the height of the cells in the first column only. otherwise, it works!
  • tacone
    tacone over 12 years
    Excellent work Neil ! Adapting it for the GridView has been a breeze ! stackoverflow.com/a/8483078/358813
  • haythem souissi
    haythem souissi about 11 years
    please how did you fixed the issue: "when i launch app with different view then layout automatically scroll at bottom"
  • Neil Traft
    Neil Traft about 11 years
    I don't know what you're referring to, Haythem.
  • bogdan
    bogdan over 10 years
    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
  • Neil Traft
    Neil Traft over 10 years
    Wrong, ListView still provides nice selection highlighting, focus traversal, and long-press features that you don't get from a LinearLayout.
  • farrukh
    farrukh about 10 years
    @ 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); } }); :)
  • Android_dev
    Android_dev almost 10 years
    Your answer is working good. but its taking too much space between rows by default how can i redues that space. - Nik Reiman
  • VSB
    VSB over 9 years
    @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).
  • Neil Traft
    Neil Traft over 9 years
    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.
  • Nitish Patel
    Nitish Patel about 9 years
    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.
  • Lionel T.
    Lionel T. about 9 years
    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.
    Lionel T. about 9 years
    Please explain more than "didn't work". What kind of Adapter are you using? I extend an ArrayAdapter wich contains an ArrayList<String> (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"
  • Nitish Patel
    Nitish Patel about 9 years
    I am not getting any error but actually my view is not getting scrolled. I am using ArrayAdapter<String> and passing string[] to it.Can you please your MainActivity with an adapter.
  • Lionel T.
    Lionel T. about 9 years
    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.
  • portfoliobuilder
    portfoliobuilder about 9 years
    This does not work for gridviews, I am receiving a binary xml file line #26 inflate error.
  • silverFoxA
    silverFoxA almost 9 years
    @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
  • Hardik Parmar
    Hardik Parmar almost 9 years
    @NeilTraft i want to scroll view inside Linear Layout and Linear Layout size is fix to inside gridview so it's possible or not ?
  • Neil Traft
    Neil Traft almost 9 years
    @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.
  • sid_09
    sid_09 about 8 years
    Friend, you saved my project and release date. Hats off to you.
  • Ajit Singh
    Ajit Singh almost 8 years
    Didn't work for me. It did not calculate the correct height. The last element is cropped.
  • Rudi Wijaya
    Rudi Wijaya over 7 years
    It is works, but not smooth for scrolling :) inside NestedScrollView
  • Maurício Giordano
    Maurício Giordano almost 7 years
    @NeilTraft nice work! It doesn't work with dividerHeight set though...
  • waka
    waka over 6 years
    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.
  • Sarith Nob
    Sarith Nob over 6 years
    Ok, Thank you! I updated the answer. Please +1 for someone who have been helped by the answer. Thank you!