Grid of images inside ScrollView
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:
<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.
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:
Write your own custom layout. This would be the harder solution (but might be considered the correct one).
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 =)