Related
I'm trying to implement a layout which contains RecyclerView and ScrollView at the same layout.
Layout template:
<RelativeLayout>
<ScrollView android:id="#+id/myScrollView">
<unrelated data>...</unrealated data>
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/my_recycler_view" />
</ScrollView>
</RelativeLayout>
Problems: I can scroll until the last element of ScrollView.
Things I tried:
Card view inside the ScrollView (now ScrollView contains RecyclerView) - can see the card up until the RecyclerView.
Initial thought was to implement this ViewGroup using RecyclerView instead of ScrollView where one of it's views type is the CardView, but I got the exact same results as with the ScrollView.
use NestedScrollView instead of ScrollView
Please go through NestedScrollView reference document for more information.
and add recyclerView.setNestedScrollingEnabled(false); to your RecyclerView
I know I am late it the game, but the issue still exists even after google has made fix on the android.support.v7.widget.RecyclerView
The issue I get now is RecyclerView with layout_height=wrap_content not taking height of all the items issue inside ScrollView that only happens on Marshmallow and Nougat+ (API 23, 24, 25) versions.
(UPDATE: Replacing ScrollView with android.support.v4.widget.NestedScrollView works on all versions. I somehow missed testing accepted solution. Added this in my github project as demo.)
After trying different things, I have found workaround that fixes this issue.
Here is my layout structure in a nutshell:
<ScrollView>
<LinearLayout> (vertical - this is the only child of scrollview)
<SomeViews>
<RecyclerView> (layout_height=wrap_content)
<SomeOtherViews>
The workaround is the wrap the RecyclerView with RelativeLayout. Don't ask me how I found this workaround!!! ¯\_(ツ)_/¯
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants">
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
Complete example is available on GitHub project - https://github.com/amardeshbd/android-recycler-view-wrap-content
Here is a demo screencast showing the fix in action:
Although the recommendation that
you should never put a scrollable view inside another scrollable view
Is a sound advice, however if you set a fixed height on the recycler view it should work fine.
If you know the height of the adapter item layout you could just calculate the height of the RecyclerView.
int viewHeight = adapterItemSize * adapterData.size();
recyclerView.getLayoutParams().height = viewHeight;
In case setting fixed height for the RecyclerView didn't work for someone (like me), here is what I've added to the fixed height solution:
mRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
#Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
int action = e.getAction();
switch (action) {
case MotionEvent.ACTION_MOVE:
rv.getParent().requestDisallowInterceptTouchEvent(true);
break;
}
return false;
}
#Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
}
#Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
});
The new Android Support Library 23.2 solves that problem, you can now set wrap_content as the height of your RecyclerView and works correctly.
Android Support Library 23.2
RecyclerViews are fine to put in ScrollViews so long as they aren't scrolling themselves. In this case, it makes sense to make it a fixed height.
The proper solution is to use wrap_content on the RecyclerView height and then implement a custom LinearLayoutManager that can properly handle the wrapping.
Copy this LinearLayoutManager into your project: link
Then wrap the RecyclerView:
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
And set it up like so:
RecyclerView list = (RecyclerView)findViewById(R.id.list);
list.setHasFixedSize(true);
list.setLayoutManager(new com.example.myapp.LinearLayoutManager(list.getContext()));
list.setAdapter(new MyViewAdapter(data));
Edit: This can cause complications with scrolling because the RecyclerView can steal the ScrollView's touch events. My solution was just to ditch the RecyclerView in all and go with a LinearLayout, programmatically inflate subviews, and add them to the layout.
For ScrollView, you could use fillViewport=true and make layout_height="match_parent" as below and put RecyclerView inside:
<ScrollView
android:fillViewport="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="#+id/llOptions">
<android.support.v7.widget.RecyclerView
android:id="#+id/rvList"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
No further height adjustment needed through code.
Try this. Very late answer, but surely helps anyone in future.
Change your ScrollView to NestedScrollView:
<android.support.v4.widget.NestedScrollView>
<android.support.v7.widget.RecyclerView
...
... />
</android.support.v4.widget.NestedScrollView>
In your UI code, update it for Recyclerview:
recyclerView.setNestedScrollingEnabled(false);
recyclerView.setHasFixedSize(false);
Calculating RecyclerView's height manually is not good, better is to use a custom LayoutManager.
The reason for above issue is any view which has it's scroll (ListView, GridView, RecyclerView) failed to calculate it's height when add as a child in another view has scroll. So overriding its onMeasure method will solve the issue.
Please replace the default layout manager with the below:
public class MyLinearLayoutManager extends android.support.v7.widget.LinearLayoutManager {
private static boolean canMakeInsetsDirty = true;
private static Field insetsDirtyField = null;
private static final int CHILD_WIDTH = 0;
private static final int CHILD_HEIGHT = 1;
private static final int DEFAULT_CHILD_SIZE = 100;
private final int[] childDimensions = new int[2];
private final RecyclerView view;
private int childSize = DEFAULT_CHILD_SIZE;
private boolean hasChildSize;
private int overScrollMode = ViewCompat.OVER_SCROLL_ALWAYS;
private final Rect tmpRect = new Rect();
#SuppressWarnings("UnusedDeclaration")
public MyLinearLayoutManager(Context context) {
super(context);
this.view = null;
}
#SuppressWarnings("UnusedDeclaration")
public MyLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
this.view = null;
}
#SuppressWarnings("UnusedDeclaration")
public MyLinearLayoutManager(RecyclerView view) {
super(view.getContext());
this.view = view;
this.overScrollMode = ViewCompat.getOverScrollMode(view);
}
#SuppressWarnings("UnusedDeclaration")
public MyLinearLayoutManager(RecyclerView view, int orientation, boolean reverseLayout) {
super(view.getContext(), orientation, reverseLayout);
this.view = view;
this.overScrollMode = ViewCompat.getOverScrollMode(view);
}
public void setOverScrollMode(int overScrollMode) {
if (overScrollMode < ViewCompat.OVER_SCROLL_ALWAYS || overScrollMode > ViewCompat.OVER_SCROLL_NEVER) {
throw new IllegalArgumentException("Unknown overscroll mode: " + overScrollMode);
}
if (this.view == null) throw new IllegalStateException("view == null");
this.overScrollMode = overScrollMode;
ViewCompat.setOverScrollMode(view, overScrollMode);
}
public static int makeUnspecifiedSpec() {
return View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
}
#Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
final boolean hasWidthSize = widthMode != View.MeasureSpec.UNSPECIFIED;
final boolean hasHeightSize = heightMode != View.MeasureSpec.UNSPECIFIED;
final boolean exactWidth = widthMode == View.MeasureSpec.EXACTLY;
final boolean exactHeight = heightMode == View.MeasureSpec.EXACTLY;
final int unspecified = makeUnspecifiedSpec();
/**
* In case of exact calculations for both dimensions let's
* use default "onMeasure" implementation.
*/
if (exactWidth && exactHeight) {
super.onMeasure(recycler, state, widthSpec, heightSpec);
return;
}
final boolean vertical = getOrientation() == VERTICAL;
initChildDimensions(widthSize, heightSize, vertical);
int width = 0;
int height = 0;
/**
* It's possible to get scrap views in recycler which are bound to old (invalid)
* adapter entities. This happens because their invalidation happens after "onMeasure"
* method. As a workaround let's clear the recycler now (it should not cause
* any performance issues while scrolling as "onMeasure" is never called whiles scrolling).
*/
recycler.clear();
final int stateItemCount = state.getItemCount();
final int adapterItemCount = getItemCount();
/**
* Adapter always contains actual data while state might contain old data
* (f.e. data before the animation is done). As we want to measure the view
* with actual data we must use data from the adapter and not from the state.
*/
for (int i = 0; i < adapterItemCount; i++) {
if (vertical) {
if (!hasChildSize) {
if (i < stateItemCount) {
/**
* We should not exceed state count, otherwise we'll get IndexOutOfBoundsException.
* For such items we will use previously calculated dimensions.
*/
measureChild(recycler, i, widthSize, unspecified, childDimensions);
} else {
logMeasureWarning(i);
}
}
height += childDimensions[CHILD_HEIGHT];
if (i == 0) {
width = childDimensions[CHILD_WIDTH];
}
if (hasHeightSize && height >= heightSize) {
break;
}
} else {
if (!hasChildSize) {
if (i < stateItemCount) {
/**
* We should not exceed state count, otherwise we'll get IndexOutOfBoundsException.
* For such items we will use previously calculated dimensions.
*/
measureChild(recycler, i, unspecified, heightSize, childDimensions);
} else {
logMeasureWarning(i);
}
}
width += childDimensions[CHILD_WIDTH];
if (i == 0) {
height = childDimensions[CHILD_HEIGHT];
}
if (hasWidthSize && width >= widthSize) {
break;
}
}
}
if (exactWidth) {
width = widthSize;
} else {
width += getPaddingLeft() + getPaddingRight();
if (hasWidthSize) {
width = Math.min(width, widthSize);
}
}
if (exactHeight) {
height = heightSize;
} else {
height += getPaddingTop() + getPaddingBottom();
if (hasHeightSize) {
height = Math.min(height, heightSize);
}
}
setMeasuredDimension(width, height);
if (view != null && overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS) {
final boolean fit = (vertical && (!hasHeightSize || height < heightSize))
|| (!vertical && (!hasWidthSize || width < widthSize));
ViewCompat.setOverScrollMode(view, fit ? ViewCompat.OVER_SCROLL_NEVER : ViewCompat.OVER_SCROLL_ALWAYS);
}
}
private void logMeasureWarning(int child) {
if (BuildConfig.DEBUG) {
Log.w("MyLinearLayoutManager", "Can't measure child #" + child + ", previously used dimensions will be reused." +
"To remove this message either use #setChildSize() method or don't run RecyclerView animations");
}
}
private void initChildDimensions(int width, int height, boolean vertical) {
if (childDimensions[CHILD_WIDTH] != 0 || childDimensions[CHILD_HEIGHT] != 0) {
/** Already initialized, skipping. */
return;
}
if (vertical) {
childDimensions[CHILD_WIDTH] = width;
childDimensions[CHILD_HEIGHT] = childSize;
} else {
childDimensions[CHILD_WIDTH] = childSize;
childDimensions[CHILD_HEIGHT] = height;
}
}
#Override
public void setOrientation(int orientation) {
/** Might be called before the constructor of this class is called. */
//noinspection ConstantConditions
if (childDimensions != null) {
if (getOrientation() != orientation) {
childDimensions[CHILD_WIDTH] = 0;
childDimensions[CHILD_HEIGHT] = 0;
}
}
super.setOrientation(orientation);
}
public void clearChildSize() {
hasChildSize = false;
setChildSize(DEFAULT_CHILD_SIZE);
}
public void setChildSize(int childSize) {
hasChildSize = true;
if (this.childSize != childSize) {
this.childSize = childSize;
requestLayout();
}
}
private void measureChild(RecyclerView.Recycler recycler, int position, int widthSize, int heightSize, int[] dimensions) {
final View child;
try {
child = recycler.getViewForPosition(position);
} catch (IndexOutOfBoundsException e) {
if (BuildConfig.DEBUG) {
Log.w("MyLinearLayoutManager", "MyLinearLayoutManager doesn't work well with animations. Consider switching them off", e);
}
return;
}
final RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) child.getLayoutParams();
final int hPadding = getPaddingLeft() + getPaddingRight();
final int vPadding = getPaddingTop() + getPaddingBottom();
final int hMargin = p.leftMargin + p.rightMargin;
final int vMargin = p.topMargin + p.bottomMargin;
/** We must make insets dirty in order calculateItemDecorationsForChild to work. */
makeInsetsDirty(p);
/** This method should be called before any getXxxDecorationXxx() methods. */
calculateItemDecorationsForChild(child, tmpRect);
final int hDecoration = getRightDecorationWidth(child) + getLeftDecorationWidth(child);
final int vDecoration = getTopDecorationHeight(child) + getBottomDecorationHeight(child);
final int childWidthSpec = getChildMeasureSpec(widthSize, hPadding + hMargin + hDecoration, p.width, canScrollHorizontally());
final int childHeightSpec = getChildMeasureSpec(heightSize, vPadding + vMargin + vDecoration, p.height, canScrollVertically());
child.measure(childWidthSpec, childHeightSpec);
dimensions[CHILD_WIDTH] = getDecoratedMeasuredWidth(child) + p.leftMargin + p.rightMargin;
dimensions[CHILD_HEIGHT] = getDecoratedMeasuredHeight(child) + p.bottomMargin + p.topMargin;
/** As view is recycled let's not keep old measured values. */
makeInsetsDirty(p);
recycler.recycleView(child);
}
private static void makeInsetsDirty(RecyclerView.LayoutParams p) {
if (!canMakeInsetsDirty) return;
try {
if (insetsDirtyField == null) {
insetsDirtyField = RecyclerView.LayoutParams.class.getDeclaredField("mInsetsDirty");
insetsDirtyField.setAccessible(true);
}
insetsDirtyField.set(p, true);
} catch (NoSuchFieldException e) {
onMakeInsertDirtyFailed();
} catch (IllegalAccessException e) {
onMakeInsertDirtyFailed();
}
}
private static void onMakeInsertDirtyFailed() {
canMakeInsetsDirty = false;
if (BuildConfig.DEBUG) {
Log.w("MyLinearLayoutManager", "Can't make LayoutParams insets dirty, decorations measurements might be incorrect");
}
}
}
If you put RecyclerView inside NestedScrollView and enable recyclerView.setNestedScrollingEnabled(false);, scrolling will working well.
However, there is a problem
RecyclerView don't recycle
For example, your RecyclerView (inside NestedScrollView or ScrollView) have 100 item.
When Activity launch, 100 item will create (onCreateViewHolder and onBindViewHolder of 100 item will called at same time).
Example, for each item, you will load a large image from API => activity created -> 100 image will load.
It make starting Activity slowness and lagging.
Possible solution:
- Thinking about using RecyclerView with multiple type.
However, if in your case, there are just a few item in RecyclerView and recycle or don't recycle don't affect performance a lot, you can use RecyclerView inside ScrollView for simple
UPDATE:
this answer is out dated now as there are widgets like NestedScrollView and RecyclerView that support nested scrolling.
you should never put a scrollable view inside another scrollable view !
i suggest you make your main layout recycler view and put your views as items of recycler view.
take a look at this example it show how to use multiple views inside recycler view adapter.
link to example
Add this line to your RecyclerView xml view:
android:nestedScrollingEnabled="false"
And your RecyclerView will be smoothly scrolled with flexible height.
Hope it helps.
It seems that NestedScrollView does solve the problem.
I've tested using this layout:
<android.support.v4.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/dummy_text" />
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp">
<android.support.v7.widget.RecyclerView
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.v7.widget.CardView>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
And it works without issues.
I was having the same problem. That's what i tried and it works. I am sharing my xml and java code. Hope this will help someone.
Here is the xml
<?xml version="1.0" encoding="utf-8"?>
< NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="#+id/iv_thumbnail"
android:layout_width="match_parent"
android:layout_height="200dp" />
<TextView
android:id="#+id/tv_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Description" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Buy" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Reviews" />
<android.support.v7.widget.RecyclerView
android:id="#+id/rc_reviews"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</android.support.v7.widget.RecyclerView>
</LinearLayout>
</NestedScrollView >
Here is the related java code. It works like a charm.
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setNestedScrollingEnabled(false);
I used CustomLayoutManager to disable RecyclerView Scrolling.
Also don't use Recycler View as WrapContent, use it as 0dp, Weight=1
public class CustomLayoutManager extends LinearLayoutManager {
private boolean isScrollEnabled;
// orientation should be LinearLayoutManager.VERTICAL or HORIZONTAL
public CustomLayoutManager(Context context, int orientation, boolean isScrollEnabled) {
super(context, orientation, false);
this.isScrollEnabled = isScrollEnabled;
}
#Override
public boolean canScrollVertically() {
//Similarly you can customize "canScrollHorizontally()" for managing horizontal scroll
return isScrollEnabled && super.canScrollVertically();
}
}
Use CustomLayoutManager in RecyclerView:
CustomLayoutManager mLayoutManager = new CustomLayoutManager(getBaseActivity(), CustomLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(mLayoutManager);
((DefaultItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
recyclerView.setAdapter(statsAdapter);
UI XML:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/background_main"
android:fillViewport="false">
<LinearLayout
android:id="#+id/contParentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<edu.aku.family_hifazat.libraries.mpchart.charts.PieChart
android:id="#+id/chart1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="#dimen/x20dp"
android:minHeight="#dimen/x300dp">
</edu.aku.family_hifazat.libraries.mpchart.charts.PieChart>
</FrameLayout>
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
</android.support.v7.widget.RecyclerView>
</LinearLayout>
</ScrollView>
Actually the main purpose of the RecyclerView is to compensate for ListView and ScrollView. Instead of doing what you're actually doing: Having a RecyclerView in a ScrollView, I would suggest having only a RecyclerView that can handle many types of children.
This does the trick:
recyclerView.setNestedScrollingEnabled(false);
First you should use NestedScrollView instead of ScrollView and put the RecyclerView inside NestedScrollView.
Use Custom layout class to measure the height and width of screen:
public class CustomLinearLayoutManager extends LinearLayoutManager {
public CustomLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
private int[] mMeasuredDimension = new int[2];
#Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state,
int widthSpec, int heightSpec) {
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
int width = 0;
int height = 0;
for (int i = 0; i < getItemCount(); i++) {
if (getOrientation() == HORIZONTAL) {
measureScrapChild(recycler, i,
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
heightSpec,
mMeasuredDimension);
width = width + mMeasuredDimension[0];
if (i == 0) {
height = mMeasuredDimension[1];
}
} else {
measureScrapChild(recycler, i,
widthSpec,
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
mMeasuredDimension);
height = height + mMeasuredDimension[1];
if (i == 0) {
width = mMeasuredDimension[0];
}
}
}
switch (widthMode) {
case View.MeasureSpec.EXACTLY:
width = widthSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
}
switch (heightMode) {
case View.MeasureSpec.EXACTLY:
height = heightSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
}
setMeasuredDimension(width, height);
}
private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec,
int heightSpec, int[] measuredDimension) {
View view = recycler.getViewForPosition(position);
recycler.bindViewToPosition(view, position);
if (view != null) {
RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
getPaddingLeft() + getPaddingRight(), p.width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
getPaddingTop() + getPaddingBottom(), p.height);
view.measure(childWidthSpec, childHeightSpec);
measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin;
measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin;
recycler.recycleView(view);
}
}
}
And implement below code in the activity/fragment of RecyclerView:
final CustomLinearLayoutManager layoutManager = new CustomLinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(mAdapter);
recyclerView.setNestedScrollingEnabled(false); // Disables scrolling for RecyclerView, CustomLinearLayoutManager used instead of MyLinearLayoutManager
recyclerView.setHasFixedSize(false);
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int visibleItemCount = layoutManager.getChildCount();
int totalItemCount = layoutManager.getItemCount();
int lastVisibleItemPos = layoutManager.findLastVisibleItemPosition();
Log.i("getChildCount", String.valueOf(visibleItemCount));
Log.i("getItemCount", String.valueOf(totalItemCount));
Log.i("lastVisibleItemPos", String.valueOf(lastVisibleItemPos));
if ((visibleItemCount + lastVisibleItemPos) >= totalItemCount) {
Log.i("LOG", "Last Item Reached!");
}
}
});
If RecyclerView showing only one row inside ScrollView. You just need to set height of your row to android:layout_height="wrap_content".
Sorry being late to the party, but it seems like there is another solution which works perfectly for the case you mentioned.
If you use a recycler view inside a recycler view, it seems to work perfectly fine. I have personally tried and used it, and it seems to give no slowness and no jerkyness at all. Now I am not sure if this is a good practice or not, but nesting multiple recycler views , even nested scroll view slows down. But this seems to work nicely. Please give it a try. I am sure nesting is going to be perfectly fine with this.
Another approach to address the issue is to use ConstraintLayout inside ScrollView:
<ScrollView>
<ConstraintLayout> (this is the only child of ScrollView)
<...Some Views...>
<RecyclerView> (layout_height=wrap_content)
<...Some Other Views...>
But I would still stick to the androidx.core.widget.NestedScrollView approach, proposed by Yang Peiyong.
You can try with setting recycler view Hight as wrap_content.
in my case its working fine. I am trying with 2 different recycler view in scroll view
The best solution is to keep multiple Views in a Single View / View Group and then keep that one view in the SrcollView. ie.
Format -
<ScrollView>
<Another View>
<RecyclerView>
<TextView>
<And Other Views>
</Another View>
</ScrollView>
Eg.
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:text="any text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:text="any text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</ScrollView>
Another Eg. of ScrollView with multiple Views
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="vertical"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFFFFF"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#string/CategoryItem"
android:textSize="20sp"
android:textColor="#000000"
/>
<TextView
android:textColor="#000000"
android:text="₹1000"
android:textSize="18sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:textColor="#000000"
android:text="so\nugh\nos\nghs\nrgh\n
sghs\noug\nhro\nghreo\nhgor\ngheroh\ngr\neoh\n
og\nhrf\ndhog\n
so\nugh\nos\nghs\nrgh\nsghs\noug\nhro\n
ghreo\nhgor\ngheroh\ngr\neoh\nog\nhrf\ndhog"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
For those people who trying to do it just for design purposes - leave it. Redesign your app and leave only RecyclerView. It will be better solution than doing ANY hardcode.
You can also override LinearLayoutManager to make RecyclerView roll smoothly:
#Override
public boolean canScrollVertically(){
return false;
}
Solution which worked for me
Use NestedScrollView with height as wrap_content, and for your RecyclerView setup this:
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
app:layoutManager="android.support.v7.widget.LinearLayoutManager" />
And set view holder layout params:
android:layout_width="match_parent"
android:layout_height="wrap_content"
I want to add a footer to the listview. When the number of list items are more,the footer works fine.
But when listview has very few items,the footer gets displayed in the middle of the screen,just below the listview .which looks shabby.In such case i want the footer to align parent bottom.
Thankyou in anticipation.
it is a simplest example of what you want. you can customize it:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<ListView
android:id="#+id/listView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="#+id/footer"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true" >
</ListView>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="90dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:id="#+id/footer" >
</RelativeLayout>
</RelativeLayout>
if you want do that you said in comment you must set layout param's of footer in code, you must get the size of your list, then get the number of row that shows in screen, then
if (listSize < numRow)
//set footer to bottom of your list
else
// android:layout_alignParentBottom="true"
Maybe your listview's height is set to wrap_content?
As far as I know the footer is added at the bottom of the listview. If you set the listview's height to match_parent it should be aligned to the bottom and also the footer should be displayed there.
(If you use a relative layout simply set listview's attribute alignParentBottom="true" in your .xml file)
You can use RecyclerView with RecyclerView.ItemDecoration to implement this behavior.
public class StickyFooterItemDecoration extends RecyclerView.ItemDecoration {
/**
* Top offset to completely hide footer from the screen and therefore avoid noticeable blink during changing position of the footer.
*/
private static final int OFF_SCREEN_OFFSET = 5000;
#Override
public void getItemOffsets(Rect outRect, final View view, final RecyclerView parent, RecyclerView.State state) {
int adapterItemCount = parent.getAdapter().getItemCount();
if (isFooter(parent, view, adapterItemCount)) {
//For the first time, each view doesn't contain any parameters related to its size,
//hence we can't calculate the appropriate offset.
//In this case, set a big top offset and notify adapter to update footer one more time.
//Also, we shouldn't do it if footer became visible after scrolling.
if (view.getHeight() == 0 && state.didStructureChange()) {
hideFooterAndUpdate(outRect, view, parent);
} else {
outRect.set(0, calculateTopOffset(parent, view, adapterItemCount), 0, 0);
}
}
}
private void hideFooterAndUpdate(Rect outRect, final View footerView, final RecyclerView parent) {
outRect.set(0, OFF_SCREEN_OFFSET, 0, 0);
footerView.post(new Runnable() {
#Override
public void run() {
parent.getAdapter().notifyDataSetChanged();
}
});
}
private int calculateTopOffset(RecyclerView parent, View footerView, int itemCount) {
int topOffset = parent.getHeight() - visibleChildsHeightWithFooter(parent, footerView, itemCount);
return topOffset < 0 ? 0 : topOffset;
}
private int visibleChildsHeightWithFooter(RecyclerView parent, View footerView, int itemCount) {
int totalHeight = 0;
//In the case of dynamic content when adding or removing are possible itemCount from the adapter is reliable,
//but when the screen can fit fewer items than in adapter, getChildCount() from RecyclerView should be used.
int onScreenItemCount = Math.min(parent.getChildCount(), itemCount);
for (int i = 0; i < onScreenItemCount - 1; i++) {
totalHeight += parent.getChildAt(i).getHeight();
}
return totalHeight + footerView.getHeight();
}
private boolean isFooter(RecyclerView parent, View view, int itemCount) {
return parent.getChildAdapterPosition(view) == itemCount - 1;
}
}
Make sure to set match_parent for the RecyclerView height.
Please have a look at the sample application https://github.com/JohnKuper/recyclerview-sticky-footer and how it works http://sendvid.com/nbpj0806
A Huge drawback of this solution is it works correctly only after notifyDataSetChanged() throughout an application(not inside decoration). With more specific notifications it won't work properly and to support them, it requires a way more logic. Also, you can get insights from the library recyclerview-stickyheaders by eowise and improve this solution.
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"/>
Answered
I have a RelativeLayout where I am adding views dynamically as the user scrolls vertically or horizontally. I have rolled my own ViewRecycler since there is potentially thousands of views that could compose the whole of what can be scrolled, but I only show 30 or so at any time. Think a zoomed in view of a calendar.
I am running into performance problems when I add the views that are about to be seen, onMeasure is called on the RelativeLayout cascading down to onMeasure getting called on all of it's child views. I already have the calculated size of how big the RelativeLayout will ever be and have set that on it's LayoutParameters, so measuring the ViewGroup isn't necessary, nor is re-measuring the Views that have already been added with their final size and the newly added view has no bearing on those view.
The simple example to demonstrate the problem is adding/removing a View to a RelativeLayout and watching the onMeasure get called despite the fact that it doesn't affect the RelativeLayout's size or the position of other Views.
main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/shell"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</LinearLayout>
MyActivity.java
public class MyActivity extends Activity
{
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
ViewGroup shell = (ViewGroup) findViewById(R.id.shell);
final RelativeLayout container = new RelativeLayout(this) {
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d("MyActvity", "onMeasure called on map");
}
};
container.setBackgroundColor(Color.rgb(255, 0, 0));
ViewGroup.LayoutParams containerParams = new ViewGroup.LayoutParams(300, 300);
final TextView childView = new TextView(this);
childView.setBackgroundColor(Color.rgb(0, 255, 0));
childView.setText("Child View");
Button viewToggle = (Button) findViewById(R.id.button);
viewToggle.setText("Add/Remove Child View");
viewToggle.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
if (childView.getParent() == null) {
container.addView(childView, 400, 30);
} else {
container.removeView(childView);
}
}
});
shell.addView(container, containerParams);
}
}
Running this, you would see 2 initial (an expected) calls to onMeasure, then one for each time that you add/remove the view by clicking the button. This obviously runs fine, but you can see where constant calls to onMeasure when you have a complex layout of nested views can get problematic.
Is there a recommended way to bypass these onMeasure calls or at least onMeasure calling measureChildren?
Instead of rolling my own Layout Manager (which I may still do in the future), I changed the onMeasure to:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
for (int i = 0; count > i; i++) {
View v = getChildAt(i);
if (v.getVisibility() != GONE) {
if (v.getMeasuredWidth() <= 0 || v.getMeasuredHeight() <= 0) {
measureChild(v,
MeasureSpec.makeMeasureSpec(v.getLayoutParams().width,
MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(v.getLayoutParams().height,
MeasureSpec.AT_MOST));
}
}
}
setMeasuredDimension(resolveSize(staticContainerWidth, widthMeasureSpec),
resolveSize(staticContainerHeight, heightMeasureSpec));
}
... and added a sudo-hard coded height and width for the container as a variable. Setting these to what you expect is out of the scope of this solution.
int staticContainerHeight = 300;
int staticContainerWidth = 300;
I ran into a similar problem when animation occurs on the size of viewgroup, whose onMeasure() gets called very frequently. Because parent view contains numerous child views, the frequent cascaded onMeasure() calls caused animation performance hiccups. I have another dirty solution but much simpler than rolling out my own layoutManager.
long mLastOnMeasurTimestamp;
...
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
long currentTimestamp = System.currentTimeMillis();
if(currentTimestamp - mLastOnMeasureTimestamp < SKIP_PERIOD_IN_MILL){
return;
}
mLastOnMeasureTimestamp = currentTimestamp;
...
I have encountered a similar problem and my solution was to check if the dimensions have changed:
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(parentWidth, parentHeight);
if ( mClientWidth == parentWidth && mClientHeight == parentHeight ) {
return;
}
mClientWidth = parentWidth;
mClientHeight = parentHeight;
So, if the parent's dimensions don't really change, it won't be cascaded down to its children.
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);