I have a custom ViewGroup that has a child ViewPager. The ViewPager is fed by a PagerAdapter that provides a LinearLayout to the ViewPager which has LayoutParams of WRAP_CONTENT on both height and width.
The view displays correctly but when the child.measure() method is called on the ViewPager it does not return the actual dimensions of the LinearLayout but seems to fill all the remaining space.
Any ideas why this is happening and how to amend it?
I wasn't very happy with the accepted answer (nor with the pre-inflate-all-views solution in the comments), so I put together a ViewPager that takes its height from the first available child. It does this by doing a second measurement pass, allowing you to steal the first child's height.
A better solution would be to make a new class inside the android.support.v4.view package that implements a better version of onMeasure (with access to package-visible methods like populate())
For the time being, though, the solution below suits me fine.
public class HeightWrappingViewPager extends ViewPager {
public HeightWrappingViewPager(Context context) {
super(context);
}
public HeightWrappingViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
boolean wrapHeight = MeasureSpec.getMode(heightMeasureSpec)
== MeasureSpec.AT_MOST;
if(wrapHeight) {
/**
* The first super.onMeasure call made the pager take up all the
* available height. Since we really wanted to wrap it, we need
* to remeasure it. Luckily, after that call the first child is
* now available. So, we take the height from it.
*/
int width = getMeasuredWidth(), height = getMeasuredHeight();
// Use the previously measured width but simplify the calculations
widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
/* If the pager actually has any children, take the first child's
* height and call that our own */
if(getChildCount() > 0) {
View firstChild = getChildAt(0);
/* The child was previously measured with exactly the full height.
* Allow it to wrap this time around. */
firstChild.measure(widthMeasureSpec,
MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
height = firstChild.getMeasuredHeight();
}
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
}
Looking at the internals of the ViewPager class in the compatibility jar:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
// For simple implementation, or internal size is always 0.
// We depend on the container to specify the layout size of
// our view. We can't really know what it is since we will be
// adding and removing different arbitrary views and do not
// want the layout to change as this happens.
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec));
...
}
It would appear that the ViewPager implementation does not measure the children views but just sets the ViewPager to be one standard view based on what the parent is passing in. When you pass wrap_content, since the view pager doesn't actually measure its content it takes up the full available area.
My recommendation would be to set a static size on your ViewPager based on the size of your child views. If this is impossible (for instance, the child views can vary) you'll either need to pick a maximum size and deal with the extra space in some views OR extend ViewPager and provide a onMeasure that measure the children. One issue you will run into is that the view pager was designed not to vary in width as different views are shown, so you'll probably be forced to pick a size and stay with it
If you setTag(position) in the instantiateItem of your PageAdapter:
#Override
public Object instantiateItem(ViewGroup collection, int page) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = (View) inflater.inflate(R.layout.page_item , null);
view.setTag(page);
then can retrieve the view (page of the adapter) with an OnPageChangeListener, measure it, and resize your ViewPager:
private ViewPager pager;
#Override
protected void onCreate(Bundle savedInstanceState) {
pager = findViewById(R.id.viewpager);
pager.setOnPageChangeListener(new SimpleOnPageChangeListener() {
#Override
public void onPageSelected(int position) {
resizePager(position);
}
});
public void resizePager(int position) {
View view = pager.findViewWithTag(position);
if (view == null)
return;
view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
//The layout params must match the parent of the ViewPager
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(width , height);
pager.setLayoutParams(params);
}
}
Following the above example I discovered that measuring the height of the child views does not always return accurate results. The solution is to measure the height of any static views (defined in the xml) and then add the height of the fragment that is dynamically created at the bottom.
In my case the static element was the PagerTitleStrip, which I also had to Override in order to enable the use of match_parent for the width in landscape mode.
So here is my take on the code from Delyan:
public class WrappingViewPager extends ViewPager {
public WrappingViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super has to be called in the beginning so the child views can be
// initialized.
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() <= 0)
return;
// Check if the selected layout_height mode is set to wrap_content
// (represented by the AT_MOST constraint).
boolean wrapHeight = MeasureSpec.getMode(heightMeasureSpec)
== MeasureSpec.AT_MOST;
int width = getMeasuredWidth();
View firstChild = getChildAt(0);
// Initially set the height to that of the first child - the
// PagerTitleStrip (since we always know that it won't be 0).
int height = firstChild.getMeasuredHeight();
if (wrapHeight) {
// Keep the current measured width.
widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
}
int fragmentHeight = 0;
fragmentHeight = measureFragment(((Fragment) getAdapter().instantiateItem(this, getCurrentItem())).getView());
// Just add the height of the fragment:
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height + fragmentHeight,
MeasureSpec.EXACTLY);
// super has to be called again so the new specs are treated as
// exact measurements.
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public int measureFragment(View view) {
if (view == null)
return 0;
view.measure(0, 0);
return view.getMeasuredHeight();
}}
And the custom PagerTitleStrip:
public class MatchingPagerTitleStrip extends android.support.v4.view.PagerTitleStrip {
public MatchingPagerTitleStrip(Context arg0, AttributeSet arg1) {
super(arg0, arg1);
}
#Override
protected void onMeasure(int arg0, int arg1) {
int size = MeasureSpec.getSize(arg0);
int newWidthSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
super.onMeasure(newWidthSpec, arg1);
}}
Cheers!
With Reference of above solutions, added some more statement to get maximum height of view pager child.
Refer the below code.
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super has to be called in the beginning so the child views can be
// initialized.
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() <= 0)
return;
// Check if the selected layout_height mode is set to wrap_content
// (represented by the AT_MOST constraint).
boolean wrapHeight = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST;
int width = getMeasuredWidth();
int childCount = getChildCount();
int height = getChildAt(0).getMeasuredHeight();
int fragmentHeight = 0;
for (int index = 0; index < childCount; index++) {
View firstChild = getChildAt(index);
// Initially set the height to that of the first child - the
// PagerTitleStrip (since we always know that it won't be 0).
height = firstChild.getMeasuredHeight() > height ? firstChild.getMeasuredHeight() : height;
int fHeight = measureFragment(((Fragment) getAdapter().instantiateItem(this, index)).getView());
fragmentHeight = fHeight > fragmentHeight ? fHeight : fragmentHeight;
}
if (wrapHeight) {
// Keep the current measured width.
widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
}
// Just add the height of the fragment:
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height + fragmentHeight, MeasureSpec.EXACTLY);
// super has to be called again so the new specs are treated as
// exact measurements.
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
better change
height = firstChild.getMeasuredHeight();
to
height = firstChild.getMeasuredHeight() + getPaddingTop() + getPaddingBottom();
Related
I am trying to set view pager inside ScrollView but it's not showing without specifying specific height.
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/svRecord"
android:fillViewport="true"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<android.support.v4.view.ViewPager
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="#+id/pager"
/>
</android.support.v4.widget.NestedScrollView>
Is there any why to set scrollview without specifying height?
I tried to set it's height 0dp and assign weight 1 but it's still not showing.
ViewPager doesn’t support wrap_content as it stands now because it doesn’t load all of its children at the same time, meaning it can’t get an appropriate measurement. So customize it like this one :)
public class MagicViewPager extends ViewPager {
public MagicViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super has to be called in the beginning so the child views can be
// initialized.
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() <= 0)
return;
// Check if the selected layout_height mode is set to wrap_content
// (represented by the AT_MOST constraint).
boolean wrapHeight = MeasureSpec.getMode(heightMeasureSpec)
== MeasureSpec.AT_MOST;
int width = getMeasuredWidth();
View firstChild = getChildAt(0);
// Initially set the height to that of the first child - the
// PagerTitleStrip (since we always know that it won't be 0).
int height = firstChild.getMeasuredHeight();
if (wrapHeight) {
// Keep the current measured width.
widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
}
int fragmentHeight = 0;
fragmentHeight = measureFragment(((Fragment) getAdapter().instantiateItem(this, getCurrentItem())).getView());
// Just add the height of the fragment:
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height + fragmentHeight,
MeasureSpec.EXACTLY);
// super has to be called again so the new specs are treated as
// exact measurements.
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public int measureFragment(View view) {
if (view == null)
return 0;
view.measure(0, 0);
return view.getMeasuredHeight();
}}
Every view declared in xml files must have the layout_height and layout_width present. If you are setting the height for the ViewPager through your java code then you can just set height to 0dp and before rendering the view make sure to set its layout parameters in the java code.
The reason for this is that ScrollView needs to know the exact height of all its child views before it can be rendered. The hight of the viewpager is not known until its pages are loaded.
i have a ViewPager which contains 3 fragments, the problem is that each fragment has a different height. In the ViewPager i have tried to use wrap_content for the height but it doesn't work.
Edit
What i need to do is to have the ViewPager to resize when i switch trough the fragments.
any idea ?
You need to set the height within the view pager, not the xml.
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.View;
public class CustomViewPager extends ViewPager {
public CustomViewPager(Context context) {
super(context);
}
public CustomViewPager(Context context, AttributeSet attrs){
super(context, attrs);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int height = 0;
for(int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
int h = child.getMeasuredHeight();
if(h > height) height = h;
}
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
Playing around with onMeasure method will get you what you need, the Viewpager is controlling the height + width of its children
If that doesn't work , you can overide the getPageHeight and GetPagewidth methods of the View Pager
#Override
public float getPageHeight(int position) {
//Return different height based on position.
// Height is returned as a float
if(position == 1)
{
return (0.35f);
}
else{
retrun 0.50f;
}
}
By telling the ViewPager to wrap_content its height will be the largest of the fragments. Then if you have other fragments that are shorter than the tallest fragment they will appear shorter inside the ViewPager. The ViewPager holds all three fragments at once - it doesn't remove the fragments one to the left and right because they need to be ready to move in.
Hence the ViewPager's height is always that of the tallest Fragment. Instead, you could set all the fragments' heights to match_parent and the ViewPager's to the size of its parent (using match_parent) or try and fit it to the maximum fragment's height using wrap_content(though I don't know how this will work when using wrap_content and match_parent together)
I am creating an app in which i am using custom gallery view (without Gallery as it is deprecated). I am using View Pager for Horizontal scroll now after overriding onMeasure() still getting extra height in view my code snippet is below
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
boolean wrapHeight = MeasureSpec.getMode(heightMeasureSpec)
== MeasureSpec.AT_MOST;
if(wrapHeight) {
int width = getMeasuredWidth(), height = getMeasuredHeight();
// Use the previously measured width but simplify the calculations
widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
/* If the pager actually has any children, take the first child's
* height and call that our own */
if(getChildCount() > 0) {
View firstChild = getChildAt(0);
/* The child was previously measured with exactly the full height.
* Allow it to wrap this time around. */
firstChild.measure(widthMeasureSpec,
MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
height = firstChild.getMeasuredHeight();
}
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
I have a custom ViewGroup that has a child ViewPager. The ViewPager is fed by a PagerAdapter that provides a LinearLayout to the ViewPager which has LayoutParams of WRAP_CONTENT on both height and width.
The view displays correctly but when the child.measure() method is called on the ViewPager it does not return the actual dimensions of the LinearLayout but seems to fill all the remaining space.
Any ideas why this is happening and how to amend it?
I wasn't very happy with the accepted answer (nor with the pre-inflate-all-views solution in the comments), so I put together a ViewPager that takes its height from the first available child. It does this by doing a second measurement pass, allowing you to steal the first child's height.
A better solution would be to make a new class inside the android.support.v4.view package that implements a better version of onMeasure (with access to package-visible methods like populate())
For the time being, though, the solution below suits me fine.
public class HeightWrappingViewPager extends ViewPager {
public HeightWrappingViewPager(Context context) {
super(context);
}
public HeightWrappingViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
boolean wrapHeight = MeasureSpec.getMode(heightMeasureSpec)
== MeasureSpec.AT_MOST;
if(wrapHeight) {
/**
* The first super.onMeasure call made the pager take up all the
* available height. Since we really wanted to wrap it, we need
* to remeasure it. Luckily, after that call the first child is
* now available. So, we take the height from it.
*/
int width = getMeasuredWidth(), height = getMeasuredHeight();
// Use the previously measured width but simplify the calculations
widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
/* If the pager actually has any children, take the first child's
* height and call that our own */
if(getChildCount() > 0) {
View firstChild = getChildAt(0);
/* The child was previously measured with exactly the full height.
* Allow it to wrap this time around. */
firstChild.measure(widthMeasureSpec,
MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
height = firstChild.getMeasuredHeight();
}
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
}
Looking at the internals of the ViewPager class in the compatibility jar:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
// For simple implementation, or internal size is always 0.
// We depend on the container to specify the layout size of
// our view. We can't really know what it is since we will be
// adding and removing different arbitrary views and do not
// want the layout to change as this happens.
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec));
...
}
It would appear that the ViewPager implementation does not measure the children views but just sets the ViewPager to be one standard view based on what the parent is passing in. When you pass wrap_content, since the view pager doesn't actually measure its content it takes up the full available area.
My recommendation would be to set a static size on your ViewPager based on the size of your child views. If this is impossible (for instance, the child views can vary) you'll either need to pick a maximum size and deal with the extra space in some views OR extend ViewPager and provide a onMeasure that measure the children. One issue you will run into is that the view pager was designed not to vary in width as different views are shown, so you'll probably be forced to pick a size and stay with it
If you setTag(position) in the instantiateItem of your PageAdapter:
#Override
public Object instantiateItem(ViewGroup collection, int page) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = (View) inflater.inflate(R.layout.page_item , null);
view.setTag(page);
then can retrieve the view (page of the adapter) with an OnPageChangeListener, measure it, and resize your ViewPager:
private ViewPager pager;
#Override
protected void onCreate(Bundle savedInstanceState) {
pager = findViewById(R.id.viewpager);
pager.setOnPageChangeListener(new SimpleOnPageChangeListener() {
#Override
public void onPageSelected(int position) {
resizePager(position);
}
});
public void resizePager(int position) {
View view = pager.findViewWithTag(position);
if (view == null)
return;
view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
//The layout params must match the parent of the ViewPager
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(width , height);
pager.setLayoutParams(params);
}
}
Following the above example I discovered that measuring the height of the child views does not always return accurate results. The solution is to measure the height of any static views (defined in the xml) and then add the height of the fragment that is dynamically created at the bottom.
In my case the static element was the PagerTitleStrip, which I also had to Override in order to enable the use of match_parent for the width in landscape mode.
So here is my take on the code from Delyan:
public class WrappingViewPager extends ViewPager {
public WrappingViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super has to be called in the beginning so the child views can be
// initialized.
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() <= 0)
return;
// Check if the selected layout_height mode is set to wrap_content
// (represented by the AT_MOST constraint).
boolean wrapHeight = MeasureSpec.getMode(heightMeasureSpec)
== MeasureSpec.AT_MOST;
int width = getMeasuredWidth();
View firstChild = getChildAt(0);
// Initially set the height to that of the first child - the
// PagerTitleStrip (since we always know that it won't be 0).
int height = firstChild.getMeasuredHeight();
if (wrapHeight) {
// Keep the current measured width.
widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
}
int fragmentHeight = 0;
fragmentHeight = measureFragment(((Fragment) getAdapter().instantiateItem(this, getCurrentItem())).getView());
// Just add the height of the fragment:
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height + fragmentHeight,
MeasureSpec.EXACTLY);
// super has to be called again so the new specs are treated as
// exact measurements.
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public int measureFragment(View view) {
if (view == null)
return 0;
view.measure(0, 0);
return view.getMeasuredHeight();
}}
And the custom PagerTitleStrip:
public class MatchingPagerTitleStrip extends android.support.v4.view.PagerTitleStrip {
public MatchingPagerTitleStrip(Context arg0, AttributeSet arg1) {
super(arg0, arg1);
}
#Override
protected void onMeasure(int arg0, int arg1) {
int size = MeasureSpec.getSize(arg0);
int newWidthSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
super.onMeasure(newWidthSpec, arg1);
}}
Cheers!
With Reference of above solutions, added some more statement to get maximum height of view pager child.
Refer the below code.
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super has to be called in the beginning so the child views can be
// initialized.
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() <= 0)
return;
// Check if the selected layout_height mode is set to wrap_content
// (represented by the AT_MOST constraint).
boolean wrapHeight = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST;
int width = getMeasuredWidth();
int childCount = getChildCount();
int height = getChildAt(0).getMeasuredHeight();
int fragmentHeight = 0;
for (int index = 0; index < childCount; index++) {
View firstChild = getChildAt(index);
// Initially set the height to that of the first child - the
// PagerTitleStrip (since we always know that it won't be 0).
height = firstChild.getMeasuredHeight() > height ? firstChild.getMeasuredHeight() : height;
int fHeight = measureFragment(((Fragment) getAdapter().instantiateItem(this, index)).getView());
fragmentHeight = fHeight > fragmentHeight ? fHeight : fragmentHeight;
}
if (wrapHeight) {
// Keep the current measured width.
widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
}
// Just add the height of the fragment:
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height + fragmentHeight, MeasureSpec.EXACTLY);
// super has to be called again so the new specs are treated as
// exact measurements.
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
better change
height = firstChild.getMeasuredHeight();
to
height = firstChild.getMeasuredHeight() + getPaddingTop() + getPaddingBottom();
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"/>