Problem description
LinearLayoutManager.scrollToPositionWithOffset(pos, 0) works great if the sum of RecyclerView's all children's height is big than screen height. But it does not work if the sum of RecyclerView's all children's height is small than screen height.
Problem description in detail
Let's say I have an Activity and a RecyclerView as it's root view. RecyclerView's width and height are both match_parent. This RecyclerView has 3 items and the sum of these 3 child view's height is small than screen height. I want to hide first item when Activity is onCreated. User will see second item at start. If user scroll down, he still can see first item. So I call LinearLayoutManager.scrollToPositionWithOffset(1, 0). But it won't work since the sum of RecyclerView's all children's height is small than screen height.
Question
How can I make RecyclerView scroll to specific position even though the sum of RecyclerView's all children's height is small than screen height.
Following is my code according to #Haran Sivaram's answer:
Item first = new Item();
Item second = new Item();
Item third = new Item();
List<Item> list = Arrays.asList(first, second, three);
adapter.add(list);
adapter.notifyDataSetChanged();
recyclerView.post(new Runnable() {
#Override
public void run() {
int sumHeight = 0;
for (int i = 0; i < recyclerView.getChildCount(); i++) {
View view = recyclerView.getChildAt(i);
sumHeight += view.getHeight();
}
if (sumHeight < recyclerView.getHeight()) {
adapter.addItem(new VerticalSpaceViewModel(recyclerView.getHeight() - sumHeight + recyclerView.getChildAt(0).getHeight()));
}
linearLayoutManager.scrollToPositionWithOffset(1, 0);
}
});
It worked. But has some small issues.
What you need to do is to increase the height of the recycler view to a minimum height which will allow scrolling and hide your first element (screen height + height of the first item). You can achieve this by adding a dummy element as the last element and setting it's height or you could also do this using padding/margins (Have not tried this).
This also needs to be done dynamically once the view is drawn (You can do it statically if you are aware of the sizes of each item beforehand - I will not recommend this method though).
Use an onGLobalLayoutListner to get a callback once the view is drawn, do your measurements here and update the height. Now the scroll with offset should work fine.
Related
I need to make sure that horizontal recyclerView height is the same as the height of the biggest item.
Items can have a different height (Item = always the same image + title + subtitle, title and subtitle could have infinite length).
When I set wrap_content for my recyclerView it would resize, basing on the height of visible items which makes content below recyclerView jump, and that's something I want to avoid.
What I want to achieve:
The gray area is visible viewport.
So basically I would like to get somehow hight of the biggest item, then put recyclerView height to that number.
What I already tried is approximation high of items based on length of title + subtitle but it's very inaccurate because for example even if two titles have the same text length they could have different width because of font that I use which is not a monospace font.
I just had this issue as well. My solution is:
Wrap the RecyclerView inside a ConstraintLayout.
Set the ConstraintLayout's layout_height to wrap_content.
Add an item view to the ConstraintLayout and populate it with the data of the item you expect to be the highest based on the length of its title for example.
Set the item view's visibility to invisible.
Set the RecyclerView's layout_height to zero, and make its top and bottom constraints match that of the item view.
Too late for an answer, but maybe this will help someone.
I struggled with the same issue and couldn't find an acceptable solution.
Solved by following:
First, you need to override onMeasure from the RecyclerView to save the largest element height:
class CustomRecycleView(ctx: Context, attrs: AttributeSet) : RecyclerView(ctx, attrs) {
private var biggestHeight: Int = 0
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
for (i in 0 until childCount) {
val child = getChildAt(i)
child.measure(widthSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED))
val h = child.measuredHeight
if (h > biggestHeight) biggestHeight = h
}
super.onMeasure(widthSpec, MeasureSpec.makeMeasureSpec(biggestHeight, MeasureSpec.EXACTLY))
}
}
In you layout replace RecycleView with this CustomRecycleView:
onMeasure is called when a new element in the list is visible, and if the element is the highest, then we save this value. For example: if the first element has lowest height but lates has highest then at start RecycleView will be have height match to first element but after scrolling it will stay match to highest.
If you don't need to make RecycleView height match to highest item at start then you can stop here.
To do this at the beginning, you must make a hack (based on #MidasLefko suggestion):
To find out initially what the height of the highest element will be, you need to add a scroll mechanism to the end and the beginning. I did it as follows:
private fun initRecycleView(items: ArrayList<Object>) {
val adapter = Adapter()
rv.visibility = View.INVISIBLE
rv.vadapter = adapter
rv.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
rv.setHasFixedSize(true)
rv.smoothScrollToPosition(pinnedPosts.size)
Handler().postDelayed({
rv.smoothScrollToPosition(0)
}, 300)
Handler().postDelayed({
rv.visibility = View.VISIBLE
}, 700)
}
Set the visibility of Recycle view to INVISIBLE and after 700 milliseconds to VISIBLE to make this process invisible for user. Also, scrolling to start is performed with a delay of 300 milliseconds, because without some delay it can work incorrectly. In my case, this is needed for a list of 3 elements, and these delays is optimal for me.
Also remember to remove all Handler callbacks in onStop ()
I don't think that this is possible out of the box.
Let's think for a minute about how a RecyclerView works. In order to save memory it reuses the same View objects and just binds them to new data from the list as the user scrolls. So, for example, if the user sees item's 0 and 1 then the system has only measured and laid out 2 items (and perhaps one or two more to help scroll performance).
But let's say that your tall item is number 50 in the list, when the RecyclerView binds the first few items it has no idea at all that item 50 even exists, let alone how tall it will be.
However, you can do something a bit hacky. For example, you can measure each items height after it is bound, keep track of the tallest, and then manually set the RecyclerView height to that size. With that mechanism in place you can make the RecyclerView be hidden, then manually scroll to the end of the list, scroll back to the beginning of the list, then show the RecyclerView.
Not the most elegant solution, but it should work.
Created a method to calculate the projected height of textView by trying all the description in the list to get the highest height.
public static int getHeightOfLargestDescription(final Context context, final CharSequence text, TextView textView) {
final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Point displaySize = new Point();
wm.getDefaultDisplay().getSize(displaySize);
final int deviceWidth = displaySize.x;
textView.setTypeface(Typeface.DEFAULT);
textView.setText(text, TextView.BufferType.SPANNABLE);
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(deviceWidth, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
textView.measure(widthMeasureSpec, heightMeasureSpec);
return textView.getMeasuredHeight();
}
then used this method to in onCreateViewHolder to get ready with the highest height to be used while binding the view.
MyViewHolder myViewHolder = new MyViewHolder(itemView);
for (Model m : modelList) {
currentItemHeight = getHeightOfLargestDescription(context, m.description, myViewHolder.description);
if (currentItemHeight > highestHeight) {
highestHeight = currentItemHeight;
}
}
Then used this highestHeight in onBindViewHolder` to set the height of the description TexView, so that all the views always have the same height that is equal to the highest height.
viewHolder.description.setHeight(highestHeight);
Code is committed in the
https://github.com/dk19121991/HorizontalRecyclerWithDynamicHeight
Let me know if this solves your problem, if you have some more question feel free to ask.
Thanks
To view a full discussion on this solution please see below
https://stackoverflow.com/a/67403898/4828650
You may try this:
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
final int newHeight = recyclerView.getMeasuredHeight();
if (0 != newHeight && minHeight < newHeight) {
// keep track the height and prevent recycler view optimizing by resizing
minHeight = newHeight;
recyclerView.setMinimumHeight(minHeight);
}
}
});
you should try with different item_view type
Try this
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = mLayoutInflater.inflate(R.layout.view_item, parent, false);
// work here if you need to control height of your items
// keep in mind that parent is RecyclerView in this case
int height = parent.getMeasuredHeight() / 4;
itemView.setMinimumHeight(height);
return new ItemViewHolder(itemView);
}
Or you can try this also
#Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View itemView = inflater.inflate(R.layout.itemview, parent, false);
ViewGroup.LayoutParams layoutParams = itemView.getLayoutParams();
layoutParams.height = (int) (parent.getHeight() * 0.3);
itemView.setLayoutParams(layoutParams);
return new MyViewHolder(itemView);
}
You can also set your itemView with fixed height.
I disabled the recycling in recycler view and it solved the issue.
recyclerView.getRecycledViewPool().setMaxRecycledViews(TYPE_CAROUSEL, 0);
this solution may have a performance issue if there are a lot of items but will work fine for a few items lets say 5 to 20 which was case for me.
recyclerViewHorizontal.setMinimumHeight(maxItemHeight) has worked well for me.
I need a dynamic gridlayout that can be toggled between 3 by 3 and 4 by 4. I can setRowCount and setColumnCount from 3 to 4 but not from 4 to 3. It will display following issue:
Caused by: java.lang.IllegalArgumentException: rowCount must be
greater than or equal to the maximum of all grid indices (and spans)
defined in the LayoutParams of each child.
Is there any work around to achieve this using gridlayout?
I realize this question is quite old, but for people who are still encountering this exception today, I'll offer an explanation that may shed some light upon how downsizing a GridLayout works and why I believe it is/was throwing an exception for the OP.
In Short:
Child views of the GridLayout can, after downsizing, occupy cells that are not within the GridLayout's grid, which is causing the IllegalArgumentException mentioned by the OP. To avoid this, remove child views that will occupy cells outside of the GridLayout's grid before actually calling setRowCount() or setColumnCount(). This can be done via GridLayout.removeView(aboutToBeIllegalChild); or by wiping the entire layout using GridLayout.removeAllViews();.
In Long:
All that calling GridLayout.setRowCount() does, is specify a new number of rows that the layout should contain. It does not, however, mess with the child views that the GridLayout currently contains, nor it's specified Spec (what column(s) and row(s) the child view occupies).
What the exception is basically telling us, and the docs confirm, is that a GridLayout does not allow any of its child views to occupy cells that are outside of the GridLayouts grid. As an example, the layout will not allow a child view to occupy cell (5, 1) when the grid is only 4 x 1.
This leads us to why the original poster was successful at dynamically increasing the GridLayout's dimensions, while being unsuccessful at decreasing it. When enlarging the dimensions, any child views that were already attached to the GridLayout with specified cells, would still be placed in legal cells if the grid received extra rows or columns dynamically. When reducing the dimensions of the grid, child views that were placed in cells that would disappear as a consequence of removing rows or columns, would now be considered illegal.
To work around this, you must either remove those (about to be) illegal child views from its parent GridLayout beforehand by calling GridLayout.removeView(aboutToBeIllegalChild); or simply wipe the entire GridLayout by calling GridLayout.removeAllViews();.
Hope this helps!
Based on Teun Kooijman answer you can just change Spec in GridLayout.LayoutParams and keep all Views inside the GridLayout:
private void changeColumnCount(int columnCount) {
if (gridLayout.getColumnCount() != columnCount) {
final int viewsCount = gridLayout.getChildCount();
for (int i = 0; i < viewsCount; i++) {
View view = gridLayout.getChildAt(i);
//new GridLayout.LayoutParams created with Spec.UNSPECIFIED
//which are package visible
view.setLayoutParams(new GridLayout.LayoutParams());
}
gridLayout.setColumnCount(columnCount);
}
}
You can also change Spec in other way by accessing GridLayout.LayoutParams.rowSpec and GridLayout.LayoutParams.columnSpec
For me, the issue was to change the number of columns of the GridView when the app changes the orientation. I achieved it by putting the bellow code in public void onConfigurationChanged(Configuration newConfig).
if (mGridLayout.getColumnCount() != getResources().getInteger(R.integer.nav_columns)) {
final int viewsCount = mGridLayout.getChildCount();
for (int i = 0; i < viewsCount; i++) {
View view = mGridLayout.getChildAt(i);
GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams();
int colIndex = i%getResources().getInteger(R.integer.nav_columns);
int rowIndex = i/getResources().getInteger(R.integer.nav_columns);
layoutParams.height = LinearLayout.LayoutParams.WRAP_CONTENT;
layoutParams.width = 0;
layoutParams.columnSpec = GridLayout.spec(colIndex,1,GridLayout.FILL,1f);
layoutParams.rowSpec = GridLayout.spec(rowIndex);
view.setLayoutParams(layoutParams);
}
mGridLayout.setColumnCount(getResources().getInteger(R.integer.nav_columns));
}
The layout parameter values may need change depending on your need.
According to #Hensin 's answer, I have modify his codes for show how to copy the previous grid items layout parameters as following:
if (gridLayout.getColumnCount() != columnCount) {
final int viewsCount = gridLayout.getChildCount();
for (int i = 0; i < viewsCount; i++) {
View view = gridLayout.getChildAt(i);
GridLayout.LayoutParams oldParams = (GridLayout.LayoutParams) view.getLayoutParams();
GridLayout.LayoutParams newParams = new GridLayout.LayoutParams();
newParams.width = oldParams.width;
newParams.height = oldParams.height;
newParams.setMargins(oldParams.leftMargin, oldParams.topMargin, oldParams.rightMargin, oldParams.bottomMargin);
view.setLayoutParams(newParams);
}
gridLayout.setColumnCount(columnCount);
}
You can now re order your items with almost the same layout parameters
I have a ListView, let's say with 10 items. Each item's layout is of height 10dp. It gives us ListView to be of height 100dp.
Now after clicking the item, I dynamically add some content to it, which causes item's height to increase by next 10dp, which in total gives 20dp height.
The problem is that when I increase the height of the item, ListView's height remains same as it was, which causes bottom item to disappear - ListView's height is 100dp whereas total height of items now is 110dp, which in the end results in disappearing item at the end.
My question is - how can I force ListView to recalculate its size based on new sizes of its items?
I tried using invalidate(), but it didn't work:
mTestsListView.setOnItemClickListener(new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> adapterView, View v, int position, long id) {
RelativeLayout basicControlsLayout = (RelativeLayout)v.findViewById(R.id.basicControlsLayout);
if(basicControlsLayout != null) {
if(basicControlsLayout.getVisibility() == View.VISIBLE) {
basicControlsLayout.setVisibility(View.GONE);
} else {
basicControlsLayout.setVisibility(View.VISIBLE);
adapterView.invalidate() // doesn't work
}
}
}
});
In the code snippet above, you can see that what I really do after clicking the item is changing visibility of a hidden layout inside it from GONE to VISIBLE, which causes item's height to increase.
try LinearLayout. ListView can handle scrollable/view reuse..., but not for dynamic height.
After setting to visible you need to call
listViewAdapter.notifyDataSetChanged();
I am trying to set parameters (margins in this case) per item in listview.
But when i do, it just sets the margin of last iteration.
How can i set properties per item (row) in the listview?
Ultimately i want to set diffrent "gaps" beween the items (so i can use it for my custom calendarview)
Eventually setDividerHeight() per item is good too, but i have the same problem on that function; namely one value for height and not a variabhle that can be changed per row.
//for loop
for (int i = 0; i < planning.size(); i++)
{
planning = getPlanning(medewerkerId, beginDate, eindDate);
int space = i * 15;
final ListView lijstje = (ListView) getActivity().findViewById(R.id.sundayList);
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) lijstje.getLayoutParams();
params.setMargins(0,space,0,0); // this wont work, and sets the height only on last iteration
lijstje.setAdapter(new PlanbordAdapter(getActivity(), R.layout.planning_item, planning));
lijstje.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
//onclick stuff here
}
}
Well to awnser my own question: Turns out i needed to use ".invalidate()" on the view in which i projected the list on, in order to redraw the items with correct margings set. hope this helps someone in the future with the same problem.
I want to be able to take a ListView and have a specific row be scrollable to the top of that Listview's bounds, even if the row is near the end and normally wouldn't be able to scroll that high in a normal android ListView (similar to how twitter works when you drill into a specific tweet and that tweet is always scrollable to the top even when there's nothing underneath it.)
Is there any way I can accomplish this task easily? I've tried measuring the row i want to scroll to the top and applying bottom padding to account for the extra space it would need, but that yields odd results (i presume because changing padding and such during the measure pass of a view is ill advised). Doing so before the measure pass doesn't work since the measured height of the cell in question (and any cells after it) hasn't happened yet.
Looks like you the setSelectionFromTop method of listview.
mListView.setSelectionFromTop(listItemIndex, 0);
I figured it out; its a bit complex but it seems to work mostly:
public int usedHeightForAndAfterDesiredRow() {
int totalHeight = 0;
for (int index = 0; index < rowHeights.size(); index++) {
int height = rowHeights.get(rowHeights.keyAt(index));
totalHeight += height;
}
return totalHeight;
}
#Override
public View getView(int position, View convertView, final ViewGroup parent) {
View view = super.getView(position, convertView, parent);
if (measuringLayout.getLayoutParams() == null) {
measuringLayout.setLayoutParams(new AbsListView.LayoutParams(parent.getWidth(), parent.getHeight()));
}
// measure the row ahead of time so that we know how much space will need to be added at the end
if (position >= mainRowPosition && position < getCount()-1 && rowHeights.indexOfKey(position) < 0) {
measuringLayout.addView(view, new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
measuringLayout.measure(MeasureSpec.makeMeasureSpec(parent.getWidth(), MeasureSpec.EXACTLY), MeasureSpec.UNSPECIFIED);
rowHeights.put(position, view.getMeasuredHeight());
measuringLayout.removeAllViews();
view.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
}
if (position == getCount()-1 && view.getLayoutParams().height == 0) {
// we know how much height the prior rows take, so calculate the last row with that.
int height = usedHeightForAndAfterDesiredRow();
height = Math.max(0, parent.getHeight() - height);
view.getLayoutParams().height = height;
}
return view;
}
This is in my adapter. It's a subclass of a merge adapter, but you can just put it in your code and substitute the super call with however you generate your rows.
the first if statement in getView() sets the layout params of a frame layout member var that is only intended for measuring, it has no parent view.
the second if statement calculates all the row heights for rows including and after the position of the row that I care about scrolling to the top. rowHeights is a SparseIntArray.
the last if statement assumes that there is one extra view with layout params already set at the bottom of the list of views whose sole intention is to be transparent and expand at will. the usedHeightForAndAfterDesiredRow call adds up all the precalculated heights which is subtracted from the parent view's height (with a min of 0 so we don't get negative heights). this ends up creating a view on the bottom that expands at will based on the heights of the other items, so a specific row can always scroll to the top of the list regardless of where it is in the list.