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.
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.
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
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+.
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.
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 =)
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();
}
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);
Related
I have a task to show fixed count of items on the screen.
It doens't mean that I have fixed size of list, it means that only 5 items should be visible when scrolling.
How it can be done?
I didn't find any useful information about it.
I also encountered a similar problem. I have solved it almost perfectly. I chose to extend LinearLayoutManager.
public class MaxCountLayoutManager extends LinearLayoutManager {
private int maxCount = -1;
public MaxCountLayoutManager(Context context) {
super(context);
}
public MaxCountLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public MaxCountLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public void setMaxCount(int maxCount) {
this.maxCount = maxCount;
}
#Override
public void setMeasuredDimension(int widthSize, int heightSize) {
int maxHeight = getMaxHeight();
if (maxHeight > 0 && maxHeight < heightSize) {
super.setMeasuredDimension(widthSize, maxHeight);
}
else {
super.setMeasuredDimension(widthSize, heightSize);
}
}
private int getMaxHeight() {
if (getChildCount() == 0 || maxCount <= 0) {
return 0;
}
View child = getChildAt(0);
int height = child.getHeight();
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
height += lp.topMargin + lp.bottomMargin;
return height*maxCount+getPaddingBottom()+getPaddingTop();
}
}
How to use:
# in kotlin
rcyclerView.layoutManager = MaxCountLayoutManager(context).apply { setMaxCount(5) }
But the height of each item needs to be the same, because I only considered the height and margin of the first item.
If i am getting your question correctly, you are trying to show a fixed number of list items on the screen, whenever the user stops scrolling.
This can be done by calculating screen height/width and then setting your list item layout dimensions(height/width), accordingly.
view.getLayoutParams().width = getScreenWidth() / VIEWS_COUNT_TO_DISPLAY;
Now, depending on whether you want a horizontal or a vertical list, change width or height values of your list item layout.
Check these links
RecyclerView number of visible items
How to show exact number of items in RecyclerView?
Simplest solution is to have onBindViewHolder() set its views height/width dynamically. For vertical list:
float containerHeight = mRecyclerView.getHeight();
holder.itemView.setMinimumHeight(Math.round(containerHeight/5));
I'm writing a calendar application for Android. The calendar needs to a have a day display similar to the default application, or MS outlook: a grid showing a line for each hour, and the appointments shown as rectangles.
Here's a similar sample image from Google Images:
I downloaded the source code for the calendar app from Google's Android Open Source Project, and saw that they implemented this display as a custom view which simplay uses Canvas.drawRect() to draw the rectangles, and then they implemented their own hit-test when the user clicks, to see which appointment was clicked.
I already wrote some of that stuff on my own and it works great, and isn't too complicated.
The problem is that I need the different lines of text inside the rectangles (the subject, the time) to be links to various functionality, and I'm wondering how I can do that.
When I draw, I already create Rects for each appointment. I was thinking I could create Rects for each piece of text as well, cache all of these in an ArrayList, and then perform the histtest against the cached rects. I'm only afraid this whole thing will be too heavy... does this sound like a solid design?
Or should I avoid the custom drawing altogether and programmatically generate and place views (TextViews maybe?) I'm an Android novice and I'm not sure what my options are...
Thanks for helping out!
Alright, as announced, here some example:
If you just use a custom view, you have to keep lists of objects and draw them yourself, as opposed to a custom layout where you just have to measure and layout the children. Since you can just add a button, there's no need to use hit-tests or whatsoever, since if you don't mess up the view will just receive the onClick() call.
Also, you can easily preview your layout in the editor if you correctly implement layout parameters. Which makes development much faster.
E.g. you can define your own layout parameters
<resources>
<declare-styleable name="TimeLineLayout_Layout">
<attr name="time_from" format="string"/>
<attr name="time_to" format="string"/>
</declare-styleable>
</resources>
Then use them like this...
<com.github.bleeding182.timelinelayout.TimeLineLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#22662222">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:background="#android:color/holo_green_dark"
android:padding="8dp"
android:text="12:00 - 16:00"
app:time_from="12:00"
app:time_to="16:00"/>
</com.github.bleeding182.timelinelayout.TimeLineLayout>
And the result would look something like this (I know it's ugly, but I made this just for testing :/ )
To do this, you create a basic layout where you measure and layout the views. You can then add any views to your layout, and by setting a time from / to and correctly measuring / layouting you can easily display all sorts of items.
The code for the screenshot is attached below, onDraw will create those ugly hour/half hour lines. onMeasure is for calculating view heights and onLayout is drawing the views to their correct time slot.
I hope this helps, it's sure easier to use than handling everything in one view.
public class TimeLineLayout extends ViewGroup {
private int tIntervalSpan = 24 * 60;
private float mMeasuredMinuteHeight;
public TimeLineLayout(Context context) {
super(context);
}
public TimeLineLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TimeLineLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public TimeLineLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
if (layoutParams instanceof LayoutParams) {
LayoutParams params = (LayoutParams) layoutParams;
final int top = (int) (params.tFrom * mMeasuredMinuteHeight);
child.layout(l, top, child.getMeasuredWidth(), top + child.getMeasuredHeight());
}
}
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
mMeasuredMinuteHeight = getMeasuredHeight() / (float) tIntervalSpan;
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
if (layoutParams instanceof LayoutParams) {
LayoutParams params = (LayoutParams) layoutParams;
child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec((int) ((params.tTo - params.tFrom) * mMeasuredMinuteHeight), MeasureSpec.EXACTLY));
}
}
}
#Override
protected void onDraw(Canvas canvas) {
final float height = mMeasuredMinuteHeight * 60;
Paint paint = new Paint();
paint.setColor(Color.RED);
for(int i = 0; i < 24; i++) {
paint.setStrokeWidth(2f);
paint.setAlpha(255);
canvas.drawLine(0, i * height, getMeasuredWidth(), i*height, paint);
if(i < 23) {
paint.setStrokeWidth(1f);
paint.setAlpha(50);
canvas.drawLine(0, i * height + 30 * mMeasuredMinuteHeight, getMeasuredWidth(), i * height + 30 * mMeasuredMinuteHeight, paint);
}
}
}
#Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
#Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
public static class LayoutParams extends ViewGroup.LayoutParams {
private final int tFrom;
private final int tTo;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.TimeLineLayout_Layout);
final String from = a.getString(R.styleable.TimeLineLayout_Layout_time_from);
final String to = a.getString(R.styleable.TimeLineLayout_Layout_time_to);
a.recycle();
tFrom = Integer.parseInt(from.split(":")[0]) * 60 + Integer.parseInt(from.split(":")[1]);
tTo = Integer.parseInt(to.split(":")[0]) * 60 + Integer.parseInt(to.split(":")[1]);
}
}
I have LinearLayout which contains two Button widgets with layout_weight=1. Depends on length of Button's text or screen resolution I get buttons with rectangular form (gray rectangles) but I need to keep the square form (blue squares).
I was trying to change height of LinearLayout param in onStart() method, depends on Button's width, but getWidth() returns 0. I understand that it's because view at that moment still not rendered. Please, help me to solve my problem.
There are many ways to achieve this. If you need to find the real width of an element, you can:
1) attach an OnGlobalLayoutListener to the view's ViewTreeObserver (but remember to remove it when you are done)
2) or you can manually measure what you need:
if(view.getMeasuredHeight() == 0){
WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics metrics = new DisplayMetrics();
manager.getDefaultDisplay().getMetrics(metrics);
view.measure( metrics.widthPixels, metrics.heightPixels );
}
int realHeight = view.getMeasuredHeight();
//...your code
You are exactly right because at that time view will not be drawn so for that you have to use viewtreeobserver:
like this:
final ViewTreeObserver treeObserver = viewToMesure
.getViewTreeObserver();
treeObserver
.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
// TODO Auto-generated method stub
System.out.println(viewToMesure.getHeight()
+ "SDAFASFASDFas");
heith = viewToMesure.getHeight();
}
});
this view tree observer will be called after creating the view based on that you calculate and you can change.
You can use a custom view to set the view's height to be tha same as its width by overriding onMeasure:
public class SquareButton extends Button {
public SquareButton (Context context) {
super(context);
}
public SquareButton (Context context, AttributeSet attrs) {
super(context, attrs);
}
public SquareButton (Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth());
}
}
All you have to is use the custom button in your xml layout and you don't have to do anything in the activity:
<com.package.name.SquareButton
android:layout_with="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
How can I make the textview wrap such text exactly ?
android:width attribute is not a solution, because the text is dynamic.
Desired behaviour
|Adcs |
|adscfd|
Current behavour:
|Adcs |
|adscfd |
Hereis the code (styles of TextViews only define things like textColor, textSize, textStyle).
<TextView
android:id="#+id/text_title_holder"
style="#style/TextBold.Black.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:maxWidth="100dp"
android:maxLines="2"
android:text="Adcs adscfd"
android:gravity="left"
android:visibility="visible" />
The topic wrap_content width on mutiline TextView has no good answer.
I have faced this problem and didn't find the solution in internet. I did this trick by creating the new component TightTextView that remeasures the given text in case you have specified the maxWidth of the component and the width of Layout (of text) is less that the measured width of the view.
package com.client.android.app.views;
import android.content.Context;
import android.text.Layout;
import android.util.AttributeSet;
import android.widget.TextView;
/**
* Tightly wraps the text when setting the maxWidth.
* #author sky
*/
public class TightTextView extends TextView {
private boolean hasMaxWidth;
public TightTextView(Context context) {
this(context, null, 0);
}
public TightTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TightTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (hasMaxWidth) {
int specModeW = MeasureSpec.getMode(widthMeasureSpec);
if (specModeW != MeasureSpec.EXACTLY) {
Layout layout = getLayout();
int linesCount = layout.getLineCount();
if (linesCount > 1) {
float textRealMaxWidth = 0;
for (int n = 0; n < linesCount; ++n) {
textRealMaxWidth = Math.max(textRealMaxWidth, layout.getLineWidth(n));
}
int w = Math.round(textRealMaxWidth);
if (w < getMeasuredWidth()) {
super.onMeasure(MeasureSpec.makeMeasureSpec(w, MeasureSpec.AT_MOST),
heightMeasureSpec);
}
}
}
}
}
#Override
public void setMaxWidth(int maxpixels) {
super.setMaxWidth(maxpixels);
hasMaxWidth = true;
}
#Override
public void setMaxEms(int maxems) {
super.setMaxEms(maxems);
hasMaxWidth = true;
}
}
!!! Just did port it to older android APIs, cuz getMaxWidth() is only available since API level 16.
This question is a little old now but I too had this problem where I wanted green text in a black box over a mapView and got around it by putting my textView in a RelativeLayout container. I then used padding to set the border size. The textView now hugs the text nicely.
My outline in eclipse looks like this.
RelativeLayout
mapview
LinearLayout
RelativeLayout
textView1 << this is the box I want to hug the text
imageView1
RelativeLayout
etc....
Hope this helps.
Like this previous person, I have unwanted overlap between GridView items:
Notice the text, in every column except the rightmost one.
Where I differ from that previous question is that I don't want a constant row height. I want the row height to vary to accommodate the tallest content in each row, for efficient use of screen space.
Looking at the source for GridView (not the authoritative copy, but kernel.org is still down), we can see in fillDown() and makeRow() that the last View seen is the "reference view": the row's height is set from the height of that View, not from the tallest one. This explains why the rightmost column is ok. Unfortunately, GridView is not well set-up for me to fix this by inheritance. All the relevant fields and methods are private.
So, before I take the well-worn bloaty path of "clone and own", is there a trick I'm missing here? I could use a TableLayout, but that would require me to implement numColumns="auto_fit" myself (since I want e.g. just one long column on a phone screen), and it also wouldn't be an AdapterView, which this feels like it ought to be.
Edit: in fact, clone and own is not practical here. GridView depends on inaccessible parts of its parent and sibling classes, and would result in importing at least 6000 lines of code (AbsListView, AdapterView, etc.)
I used a static array to drive max heights for the row. This is not perfect since the earlier columns will not be resized until the cell is redisplayed. Here is the code for the inflated reusable content view.
Edit: I got this work correctly but I had pre-measure all cells before rendering. I did this by subclassing GridView and adding a measuring hook in the onLayout method.
/**
* Custom view group that shares a common max height
* #author Chase Colburn
*/
public class GridViewItemLayout extends LinearLayout {
// Array of max cell heights for each row
private static int[] mMaxRowHeight;
// The number of columns in the grid view
private static int mNumColumns;
// The position of the view cell
private int mPosition;
// Public constructor
public GridViewItemLayout(Context context) {
super(context);
}
// Public constructor
public GridViewItemLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* Set the position of the view cell
* #param position
*/
public void setPosition(int position) {
mPosition = position;
}
/**
* Set the number of columns and item count in order to accurately store the
* max height for each row. This must be called whenever there is a change to the layout
* or content data.
*
* #param numColumns
* #param itemCount
*/
public static void initItemLayout(int numColumns, int itemCount) {
mNumColumns = numColumns;
mMaxRowHeight = new int[itemCount];
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// Do not calculate max height if column count is only one
if(mNumColumns <= 1 || mMaxRowHeight == null) {
return;
}
// Get the current view cell index for the grid row
int rowIndex = mPosition / mNumColumns;
// Get the measured height for this layout
int measuredHeight = getMeasuredHeight();
// If the current height is larger than previous measurements, update the array
if(measuredHeight > mMaxRowHeight[rowIndex]) {
mMaxRowHeight[rowIndex] = measuredHeight;
}
// Update the dimensions of the layout to reflect the max height
setMeasuredDimension(getMeasuredWidth(), mMaxRowHeight[rowIndex]);
}
}
Here is the measuring function in my BaseAdapter subclass. Note that I have a method updateItemDisplay that sets all appropriate text and images on the view cell.
/**
* Run a pass through each item and force a measure to determine the max height for each row
*/
public void measureItems(int columnWidth) {
// Obtain system inflater
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// Inflate temp layout object for measuring
GridViewItemLayout itemView = (GridViewItemLayout)inflater.inflate(R.layout.list_confirm_item, null);
// Create measuring specs
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(columnWidth, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
// Loop through each data object
for(int index = 0; index < mItems.size(); index++) {
String[] item = mItems.get(index);
// Set position and data
itemView.setPosition(index);
itemView.updateItemDisplay(item, mLanguage);
// Force measuring
itemView.requestLayout();
itemView.measure(widthMeasureSpec, heightMeasureSpec);
}
}
And finally, here is the GridView subclass set up to measure view cells during layout:
/**
* Custom subclass of grid view to measure all view cells
* in order to determine the max height of the row
*
* #author Chase Colburn
*/
public class AutoMeasureGridView extends GridView {
public AutoMeasureGridView(Context context) {
super(context);
}
public AutoMeasureGridView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AutoMeasureGridView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if(changed) {
CustomAdapter adapter = (CustomAdapter)getAdapter();
int numColumns = getContext().getResources().getInteger(R.integer.list_num_columns);
GridViewItemLayout.initItemLayout(numColumns, adapter.getCount());
if(numColumns > 1) {
int columnWidth = getMeasuredWidth() / numColumns;
adapter.measureItems(columnWidth);
}
}
super.onLayout(changed, l, t, r, b);
}
}
The reason I have the number of columns as a resource is so that I can have a different number based on orientation, etc.
Based on the info from Chris, I used this workaround making use of the reference-View used by the native GridView when determining the height of other GridView items.
I created this GridViewItemContainer custom class:
/**
* This class makes sure that all items in a GridView row are of the same height.
* (Could extend FrameLayout, LinearLayout etc as well, RelativeLayout was just my choice here)
* #author Anton Spaans
*
*/
public class GridViewItemContainer extends RelativeLayout {
private View[] viewsInRow;
public GridViewItemContainer(Context context) {
super(context);
}
public GridViewItemContainer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public GridViewItemContainer(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setViewsInRow(View[] viewsInRow) {
if (viewsInRow != null) {
if (this.viewsInRow == null) {
this.viewsInRow = Arrays.copyOf(viewsInRow, viewsInRow.length);
}
else {
System.arraycopy(viewsInRow, 0, this.viewsInRow, 0, viewsInRow.length);
}
}
else if (this.viewsInRow != null){
Arrays.fill(this.viewsInRow, null);
}
}
#Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (viewsInRow == null) {
return;
}
int measuredHeight = getMeasuredHeight();
int maxHeight = measuredHeight;
for (View siblingInRow : viewsInRow) {
if (siblingInRow != null) {
maxHeight = Math.max(maxHeight, siblingInRow.getMeasuredHeight());
}
}
if (maxHeight == measuredHeight) {
return;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
switch(heightMode) {
case MeasureSpec.AT_MOST:
heightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.min(maxHeight, heightSize), MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
break;
case MeasureSpec.EXACTLY:
// No debate here. Final measuring already took place. That's it.
break;
case MeasureSpec.UNSPECIFIED:
heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
break;
}
}
In your adapter's getView method, either wrap your convertView as a child in a new GridViewItemContainer or make this one the top XML element of your item layout:
// convertView has been just been inflated or came from getView parameter.
if (!(convertView instanceof GridViewItemContainer)) {
ViewGroup container = new GridViewItemContainer(inflater.getContext());
// If you have tags, move them to the new top element. E.g.:
container.setTag(convertView.getTag());
convertView.setTag(null);
container.addView(convertView);
convertView = container;
}
...
...
viewsInRow[position % numColumns] = convertView;
GridViewItemContainer referenceView = (GridViewItemContainer)convertView;
if ((position % numColumns == (numColumns-1)) || (position == getCount()-1)) {
referenceView.setViewsInRow(viewsInRow);
}
else {
referenceView.setViewsInRow(null);
}
Where numColumns is the number of columns in the GridView and 'viewsInRow' is an list of View on the current row of where 'position' is located.
I did so many research but found incomplete answer or had tough with understanding what going on with solution but finally found an answer that fit perfectly with proper explanation.
My problem was to fit gridview item into height properly. This Grid-view worked great when all of your views are the same height. But when your views have different heights, the grid doesn't behave as expected. Views will overlap each other, causing an an-aesthetically pleasing grid.
Here Solution I used this class in XML layout.
I used this solution, and this is working very well, thanks a lot.--Abhishek Mittal
If you convert your GridView or ListView to a RecyclerView, this issue will not happen. And you won't need to make a custom GridView class.
This is not the correct solution which I am mentioned below, but can be workaround depends on your requirement.
Just set the height of view fix(in some dp i.e.- 50dp) from your child layout of gridview, so that it can be Wrapped.
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:ellipsize="end"
android:textColor="#color/text_color"
android:textSize="13dp"
android:textStyle="normal" />
Giving weight to your GridView also works on GridViews inside LinearLayouts as a child. This way GridView fills the viewport with its children so you are able to view it's items as long as they fit the screen (then you scroll).
But always avoid using GridViews inside ScrollViews. Otherwise you will need to calculate each child's height and reassign them as Chase answered above.
<GridView
android:id="#+id/gvFriends"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:verticalSpacing="5dp"
android:horizontalSpacing="5dp"
android:clipChildren="false"
android:listSelector="#android:color/transparent"
android:scrollbarAlwaysDrawHorizontalTrack="false"
android:scrollbarAlwaysDrawVerticalTrack="false"
android:stretchMode="columnWidth"
android:scrollbars="none"
android:numColumns="4"/>