RecyclerView in a RecyclerView item - android

I know it's a bad idea, but I'm using a RecyclerView inside another RecyclerView's item to display a list of items containing a list.
I managed to get it work and tweaked it as far as I could, but I still get those green spikes when lists with a bigger number of items begin to show and are being drawn on the screen.
Those spikes produce a noticeable lag when flinging through items and are even more obvious when using something slower and older than Samsung Galaxy S6.
I did some logging, and it appears that the outer ViewHolders are being recycled and the inner RecyclerView's adapter is created only 5 times so it must be the drawing time that kills the performance.
I've managed to limit the creation of new inner RecyclerView's ViewHolders by setting the maximum number of ViewHolders in the RecycledViewPool but that helped only a little.
I know that the inner RecyclerView doesn't have a "bottom" on which it could rely to calculate when to show new items so it has to draw every item immediately and that is probably the main reason why one shouldn't use this setup. (I come to my senses as I write this question.. thank you rubber ducks of the world).
Is there a way this could be tweaked even further or could you suggest how to make this work using some different setup other than RecyclerView inside another RecyclerView's item?
I will provide more info if needed.
Thank you

The best solution is to either switch back to a ListView and use the ExpandableListView interface, or to implement it yourself on RecyclerView.
As you mentioned - listing scroll components is never a good solution. Here is an example expandable list view adapter so you have an idea what would be required:
public class MyExpandableListAdapter extends BaseExpandableListAdapter {
...
#Override
public Object getChild(int listPosition, int childListPosition) {
//return an item that would have been in one of the nested recyclers
//(listPosition = parent, childListPosition = nested item number)
return getGroup(listPosition).getChildren().get(childListPosition);
}
#Override
public int getChildrenCount(int listPosition) {
//presuming the parent items contain the children
return getGroup(listPosition).getChildren().size();
}
#Override
public Object getGroup(int listPosition) {
//group is the parent items (the tope level recycler view items)
return mData.get(listPosition);
}
#Override
public int getGroupCount() {
return mData.size();
}
#Override
public View getGroupView(int listPosition, boolean isExpanded, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.list_item, parent, false);
}
MyDataType item = getGroup(listPosition);
//set the fields (or better yet, use viewholder pattern)
return convertView;
}
#Override
public View getChildView(int listPosition, final int expandedListPosition, boolean isLastChild, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.list_item, parent, false);
}
MyDataType item = getChild(listPosition, expandedListPosition);
//set the fields (or better yet, use viewholder pattern)
return convertView;
}
}

I managed to get better performance by creating a shared RecycledViewPool, as it was done in a project #Raghunandan linked in the comments.
Google I/O Android App - Shared RecycledViewsPool
I also set the max number of views to that Pool which really did the trick, now I just have to find a way to reuse my listeners and avoid creating new ones, but that's a different issue.
Thank you all for responding.
I will post more if I find anything useful in the future.

Related

ListView and it's "weird" behavior. How can i solve this situation?

As you should know, ListView recycles the view. But i want to work with elements that can be clicked and expanded. Like i already did:
But it was completely messed up, even using:
View checklayout = convertView;
if(checklayout == null){
checklayout = inflater.inflate(R.layout.home_cell, null);
}
When some opened expandable views goes out of the screen, the recycled one, which shouldn't be expandable, receives the vanished's layout. Only view that has "1 AVALIAÇÃO LANÇADA" should open, and show it's content. I add this content by using if(qtdAvaliacoes > 0) that is a property of my Object that comes from ArrayList<>.
I "solved" this disabling the recycler, with:
#Override
public int getViewTypeCount() {
return getCount();
}
#Override
public int getItemViewType(int position) {
return position;
}
Once my listView will only receives 5~10 rows. But i know that isn't a good practice. While i'm writting this question, i found a solution, calling my object before inflate any view, then checking the property:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View checklayout = convertView;
final LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final Disciplina disciplina = lista.get(position);
if(checklayout == null || disciplina.getQtdAvaliacoes() == 0){
checklayout = inflater.inflate(R.layout.home_cell, null);
}
final View layout = checklayout;
But I don't think this is the best way to do this. I read something about Tags, but was little confused. I think if i could bind these onClick methods to the row position it would be better.
Any ideas ? Or is my solution good at you, developer's, point of view.
Thanks.
The easiest way is to not do subinflates within a list item. Do it via view visibilities instead, making the inflated part GONE if you don't want it to display yet. You'll just have to explicitly set the visibility of that view in every call to getView

Android get convertView from BaseExpandableListAdapter

I need to call the BaseExpandableListAdapter method
#Override
public View getChildView(int groupPosition, final int childPosition, boolean isLastChild, View convertView, ViewGroup parent)
or something like that to get the view that the method returns. The problem is, if I call that method, I have to pass null to convertView, which will reinflate the view, which is the view I need.
I need the view because it contains a custom view with code I need to retrieve from it without resetting it to a new instance of itself. Take this method for example:
public ArrayList<CustomViewData> getCustomViewData ()
{
ArrayList<CustomViewData> customViewDatas = new ArrayList<>();
for(int i = 0; i < getGroupCount(); i++)
if(listView.isGroupExpanded(i))
{
View v = //getConvertView
customViewDatas.add((CustomViewData) v.findViewById(R.id.customView);
}
return customViewDatas;
}
How can I get the actual view from the ExpandableListView (or its base adapter) without creating a new view?
Never store the Views generated by getView(). This is a very dangerous practice. You risk leaking Views. Additionally the adapter recycles Views as you scroll, so there's a chance what you are storing will not be representative of what you need.
You are thinking in the wrong direction. The adapter binds data to Views. Meaning, how a View is rendered is based upon the data itself. If you need to update one of the Views as a result of some event...then the data representative of that View must be updated. The adapter can then re-render it's View accordingly.
When solving such problems, just keep telling yourself "If I need to modify the View, then I need to modify it's data."
The solution to this problem is not an easily apparent one.
There is no way to replicate behaviors like ArrayAdapter's capabilities for RecyclerView Adapters.
Instead, if you need to get data from a custom view that is not automatically stored, override
#Override
public void onViewDetachedFromWindow (ViewHolder holder)
{
//STORE VIEW DATA
super.onViewDetachedFromWindow(holder);
}
#Override
public void onViewRecycled(ViewHolder holder)
{
//STORE VIEW DATA
super.onViewRecycled(holder);
}
Use those methods to get the data from the view stored in your ViewHolder.
You must get the data from the visible views after you are done with the recycler. Those visible views are not recycled automatically so you have to retreive the view data from the visible views still on the adapter.
To do this, see this post.

ListView: how to access Item's elements programmatically from outside?

I have the following situation.
I have a ListView, each item of the ListView is comprised of different widgets (TextViews, ImageViews, etc...) inflated form a Layout in the getView() method of the custom adapter.
Now, I would like to achieve the following:
when a certain event is triggered I want to change the background of a View which is inside the item.
Please how do I do it?
This is the the Item Layout:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/cardlayout"
android:layout_width="320dp"
android:layout_height="130dp"
android:background="#android:color/transparent"
android:orientation="vertical"
android:paddingBottom="5dp"
android:paddingRight="5dp"
android:paddingTop="5dp" >
<FrameLayout
android:layout_width="320dp"
android:layout_height="117dp" >
<View
android:id="#+id/card"
android:layout_width="320dp"
android:layout_height="117dp"
android:background="#drawable/card_selector" />
</FrameLayout>
</LinearLayout>
I need to change the background of card
I have tried doing this:
View v=lv.getAdapter().getView(index, null, lv);
View card =(View)v.findViewById(R.id.card);
card.setBackgroundResource(R.drawable.pressed_background_card);
But no success :-((
When your event is triggered you should just call a notifyDataSetChanged on your adapter so that it will call again getView for all your visible elements.
Your getView method should take into account that some elements may have different background colors (and not forget to set it to normal color if the element doesn't need the changed background, else with recycling you would have many elements with changed background when you scroll)
edit :
I would try something like this :
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView == null)
{
convertView = LayoutInflater.from(getContext()).inflate(R.layout.card, parent, false);
}
//This part should also be optimised with a ViewHolder
//because findViewById is a costly operation, but that's not the point of this example
CardView cardView =(CardView)convertView .findViewById(R.id.card);
//I suppose your card should be determined by your adapter, not a new one each time
Card card = getItem(position);
//here you should check sthg like the position presence in a map or a special state of your card object
if(mapCardWithSpecialBackground.contains(position))
{
card.setBackgroundResource(specialBackground);
}
else
{
card.setBackgroundResource(normalBackground);
}
cardView.setCard(card);
return convertView;
}
And on the special event i would add the position of the item into the map and call notifyDataSetChanged.
Use the onitemclicklistener which has method onclicksomething..that takes four or five parameters. (View parent, View view, int position, int id). Use the view parameter to customize your background.
Update
Here's some of my code, If you don't understand I recommend to read about recycling and ViewHolder pattern.
#Override
public View getView(int position, View convertView, ViewGroup parent) {
{
ViewHolder viewHolder;
// If convertView isn't a recycled view, create a new.
if(convertView == null){
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.row_gallery_frame, parent, false);
viewHolder = new ViewHolder();
// Here you must be able to find your Widget inside convertView and set a listener to it I guess?
viewHolder.nameHolder = (TextView) convertView.findViewById(R.id.nameTv);
// Set a reference to newly inflated view
convertView.setTag(viewHolder);
}
// If it is, then get the ViewHolder by tag
else{
viewHolder = (ViewHolder)convertView.getTag();
}
// Set the data
GalleryFrame galleryFrame = galleryFrameArrayList.get(position);
viewHolder.nameHolder.setText(galleryFrame.getName());
return convertView;
}
}
// Viewholder pattern which holds all widgets used
public static class ViewHolder{
public TextView nameHolder;
}
I assume you have a model object that you use to "draw" the list item , and for example the background color is determined based on a boolean or something.
All you need to do, is change the value on which you base your decision which background color should that TextView have.
Your getView() method should have code like that
if (myModelObj.isBrown()) {
myTextView.setBackgroundResource(R.drawable.brown_bg);
else
myTextView.setBackgroundResource(R.drawable.not_brown_bg);
All you should do when ur event is triggered, is set the value of the brown boolean in your model
and call notifyDataSetChanged() on your adapter
EDIT
If for some reason you don't wanna call nofitfyDataSetChanged(), althought it won't move the scroll position of your list and with the right recyclying it won't cause bad performance
You can find the View object that represent the list item you want to edit-if it's visisble-, and simply change the background in it, without refreshing the list at all.
int wantedPosition = 10; // Whatever position you're looking for
int firstPosition = listView.getFirstVisiblePosition() - listView.getHeaderViewsCount();
int wantedChild = wantedPosition - firstPosition
if (wantedChild < 0 || wantedChild >= listView.getChildCount()) {
// Wanted item isn't displayed
return;
}
View wantedView = listView.getChildAt(wantedChild);
then use wantedView to edit your background
This answer can be found here
try this one:
View v=lv.getAdapter().getView(index, null, lv);
View card =(View)v.findViewById(R.id.card);
card.setBackgroundResource(R.drawable.pressed_background_card);
card.invalidate();
v.invalidate();
those function force your views to redraw itself and they will render again.
look at invalidate()
What I normally do is this:
public static class EventDetailsRenderer {
private TextView title;
private TextView description;
private Event item;
public EventDetailsRenderer(View view) {
extractFromView(view);
}
private final void extractFromView(View view) {
title = (TextView) view.findViewById(R.id.EventTitle);
description = (TextView) view.findViewById(R.id.Description);
}
public final void render() {
render(item);
}
public final void render(Event item) {
this.item= item;
title.setText(item.getTitle());
description.setText(item.getDescription());
}
}
private class EventsAdapter
extends ArrayAdapter<Event> {
public EventsAdapter(Context context) {
super(context, R.layout.list_node__event_details, 0);
}
public void addAllItems(Event... services) {
for (int i = 0; i < services.length; i++) {
add(services[i]);
}
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
Event event = getItem(position);
EventDetailsRenderer eventRenderer;
if (convertView != null && convertView.getTag() != null) {
eventRenderer = (EventDetailsRenderer) convertView.getTag();
} else {
convertView = getActivity().getLayoutInflater().inflate(R.layout.list_node__event_details, null);
eventRenderer = new EventDetailsRenderer(convertView);
convertView.setTag(eventRenderer);
}
eventRenderer.render(event);
return convertView;
}
}
NOTE: that this example might not compile I pasted it from some code I have and deleted some lines to show an example but the logic it the same.
And then when you want to render it, just get the children from the list, iterate over them, check if the renderer contains the card you want to flip and call its render method... then you render a specific item in the list without effecting the rest of the items.
Let me know if this works...
Adam.
User EasyListViewAdapters library https://github.com/birajpatel/EasyListViewAdapters
Features
Easier than implementing your own Adapter (ie handling
BaseAdaper#getView).Very Easier to provide multi-row support.
Library takes care of recycling all views, that ensures performance
& helps your list view scroll smoothly.
Cleaner code. By keeping different RowViewSetter classes for
different row-types makes your code easy to manage & easy to reuse.
No data browsing, Library takes care of browsing data through
data-structure when View is being drawn or event occurs so that
Users does not have to look for their data to take actions.
Just by passing correct row-types library will Auto-map your
data-types to row-types to render views. Row views can be created by
using XML or Java (doesn't restrict to XML-Only Approach).
Load More callbacks can be registered to implement paginatation
support to your list.
Handling children viewclicks, you can also register for
Children(present inside your rows) view click events.
All these Views are registered with single OnClickListner so that
this mechanism is very memory efficient when click event occurs
users you gets clickedChildView, rowData,int eventId as callback
params.

the getChildView method run 2 times in Expandable ListView

i am creating an app which contains an expandable list view. the child views are created dynamically. and it run successfully. During debugging i found that the getChildview function runs 2 times.
i create dynamic layouts and put it into a list. when the getChildView runs 2 times the layouts added 2 times in to the list..
getChildView() is not an appropriate place to create children. It might be called pretty often. The rendering process needs to visit children twice, anyway.
It's not possible to judge where the appropriate place would be for adding children to your list, or even if your list approach is the right way to do it, without much more information.
If you regenerate the list in group click, then removing it can be a solution. For example, in the following code, the getChildView() always called twice because of myList.expandGroup(groupPosition).
public boolean onGroupClick(ExpandableListView parent, View v,
int groupPosition, long id) {
//get the group header
HeaderInfo headerInfo = medicationDate.get(groupPosition);
myList.expandGroup(groupPosition);
//set the current group to be selected so that it becomes visible
//myList.setSelectedGroup(groupPosition);
//display it or do something with it
Toast.makeText(getBaseContext(), "Child on Header " + headerInfo.getHeaderInfo()+"with childsize"+headerInfo.getChildInfo().size(),
Toast.LENGTH_SHORT).show();
return false;
}
I'm new to android development and maybe wrong, but as I see it getChildView() has 4th argument View convertView which is null first time view need to be rendered. Once created it is stored and reused again when needed. So if you create new views in getChildView() it is enough to have something like this
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
if (convertView != null) {
// View is already created here, update it if you like
return convertView;
}
// Else create your view(s) here and return the root of view container as usual
...
return convertView; // or whatever your root view is
}
The height of the listview should be match_parent instead of wrap_content.
one thing worked for me was.
#Override
public boolean hasStableIds() {
// To avoid refreshing return true and makesure Ids each position have same view.
return true;
//return false;
}

Problem setting row backgrounds in Android Listview

I have an application in which I'd like one row at a time to have a certain color. This seems to work about 95% of the time, but sometimes instead of having just one row with this color, it will allow multiple rows to have the color. Specifically, a row is set to have the "special" color when it is tapped. In rare instances, the last row tapped will retain the color despite a call to setBackgroundColor attempting to make it otherwise.
private OnItemClickListener mDirectoryListener = new OnItemClickListener(){
public void onItemClick(AdapterView parent, View view, int pos, long id){
if (stdir.getStationCount() == pos) {
stdir.moreStations();
return;
}
if (playingView != null)
playingView.setBackgroundColor(Color.DKGRAY);
view.setBackgroundColor(Color.MAGENTA);
playingView = view;
playStation(pos);
}
};
I have confirmed with print statements that the code setting the row to gray is always called.
Can anyone imagine a reason why this code might intermittently fail? If there is a pattern or condition that causes it, I can't tell.
I thought it might have something to do with the activity lifecycle setting the "playingView" variable back to null, but I can't reliably reproduce the problem by switching activities or locking the phone.
private class DirectoryAdapter extends ArrayAdapter {
private ArrayList<Station> items;
public DirectoryAdapter(Context c, int resLayoutId, ArrayList<Station> stations){
super(c, resLayoutId, stations);
this.items = stations;
}
public int getCount(){
return items.size() + 1;
}
public View getView(int position, View convertView, ViewGroup parent){
View v = convertView;
LayoutInflater vi = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (position == this.items.size()) {
v = vi.inflate(R.layout.morerow, null);
return v;
}
Station station = this.items.get(position);
v = vi.inflate(R.layout.songrow, null);
if (station.playing)
v.setBackgroundColor(Color.MAGENTA);
else if (station.visited)
v.setBackgroundColor(Color.DKGRAY);
else
v.setBackgroundColor(Color.BLACK);
TextView title = (TextView)v.findViewById(R.id.title);
title.setText(station.name);
return v;
}
};
ListViews don't create instances of contained views for every item in the list, but only for ones that are actually visible on the screen. For performance reasons, they try and maintain as few views as possible, and recycle them. That's what the convertView parameter is.
When a view scrolls off the screen, it may be recycled or destroyed. You can't hold a reference to an old view and assume that it will refer to the item you expect it to in the future. You should save the ID of the list item you need and look that up instead.
Moreover, there are a couple of other issues with your implementation (from a best practices standpoint). You seem to be ignoring the convertView parameter and creating a new view from scratch each time. That can cause your application to bog down a bit while scrolling if you have a long list. Secondly, instead of adding the "more" element the way you do, you're better of setting it with setFooterView().
There's an excellent talk on the ListView from Google I/O 2010 that covers these and other issues. It's an hour long, but definitely worth watching in its entirety.

Categories

Resources