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.
Related
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.
Basically I have 2 Views that both use an Adapter to populate.
An ExpandableListView. ExpandableListView gridHostList;
A custom View that works very similarly to GridView (it's called StaggeredGridView) StaggeredGridView grid;
Their Adapters in order:
HostListAdapter which inherits from BaseExpandableListAdapter.
GridAdapter which inherits from BaseAdapter.
The reason for this is I need an expandable list with varying numbers of parents or groups where each group contains a grid that also has a varying number of children. I looked into many ways of achieving this and I think this is the best but I am open to alternatives.
In the getChildView() method of HostListAdapter I make my grid and this works fine.
#Override
public View getChildView(int groupPosition, final int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
convertView = mInflater.inflate(R.layout.layout_containing_grid, null);
StaggeredGridView gridView = (StaggeredGridView) convertView.findViewById(R.id.grid_view);
}
Now when I set the adapter of my StaggeredGridView here as shown below everything works as expected.
#Override
public View getChildView(int groupPosition, final int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
convertView = mInflater.inflate(R.layout.layout_containing_grid, null);
StaggeredGridView gridView = (StaggeredGridView) convertView.findViewById(R.id.grid_view);
gridView.setAdapter(mGridAdapter);
}
That works fine. However I want to set the adapter from the Fragment which contains all of this (it's in a ViewPager). To do this I store the view returned by the hostListAdapter in a Hashmap with the Hashmap key being a Class I made that contains an ArrayList.
gridHostList = (GridHostList) V.findViewById(R.id.gridHostList);
if (!gridsHashMap.containsKey(dataForThisGrid)) {
gridsHashMap.put(dataForThisGrid, getGrid(0, gridsHashMap.get(dataForThisGrid)));
StaggeredGridView gridView = (StaggeredGridView) gridsHashMap.get(dataForThisGrid).findViewById(R.id.grid_view);
if (gridView.getAdapter() == null) {
gridView.setAdapter(new GridAdapter(context);
}
private View getGrid(int groupPosition, View convertView) {
View v;
v = gridHostList.getExpandableListAdapter().getChildView(groupPosition, 0, true, convertView, gridHostList);
return v;
}
}
I've tried this without any of the fancy HashMap stuff or logic of any kind to check if that was the problem and found nothing. I know I am actually getting a View from the parent adapter. gridAdapter.getCount() is being called by the system and returning a positive int as it should. However the problem seems to come because for some reason gridAdapter.getView is never called.
So what I am trying to figure out is why gridAdapter.getView is called when I set it in the hostListAdapter, but not when I set it in the Fragment which contains this all.
Please forgive minor errors in the code here, I made many changes in the StackOverflow editor (mostly removing logs and changing names for clarity). Thanks for your time.
In your getGrid method you are getting the StaggeredGridView and you are adding it to the gridsHashMap with the key dataForThisGrid . But when you are retrieving it from the gridsHashMap you are doing it as (StaggeredGridView) gridsHashMap.get(dataForThisGrid).findViewById(R.id.grid_view);.
I think you have to retrieve it using just
(StaggeredGridView) gridsHashMap.get(dataForThisGrid);
Note: I have seen people advising not to call the getView or getChildView method directly as calling them will result in inflating the respective view.
In my android app my Main Activity extends List Activity. I store there elements called Items, their layout is defined by itemLayout xml file and I use custom adapter (called ItemAdapter) to transfer data to List View in Main Activity. In itemLayout there's an ImageView and my aim is to change its image when user clicks on the particular item in list view. In my opinion the easiest way to achieve that is to get access to particular item's (the one that was clicked) layout, there find the ImageView and call method setImageBitmap. How can I "find" this layout of clicked item? I tried many things in method:
#Override
protected void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
}
but nothing worked. Do I have to add anything to my ItemAdapter? Or is it possible to do in onListItemClick(…) but I can't find out how?
You're thinking about it in slightly the wrong way. Adapter's are a way to map data to views. So if you want to change how a particular view looks for a given position, you need to change it's correlating data so that the rendered View then changes. Attempting to modify a view directly kinda goes against how adapters are meant to be used.
So in your case, when a user clicks on an item. Find that item in your adapter via the position number. Then update the item and ensure notifydataset is called. Meanwhile, your adapter's getView() will then handle displaying the appropriate image.
As I understood you need to modify clicked item layout, right? Use argument from onListItemClicked:
v.findViewById(<your_img_view_id>)
For better performance use view holder.
onListItemClick is fired when you press on an element of the ListView. If you want to retrieve the element in the dataset, you can simply invoke
l.getItemAtPosition(position)
the returned value has to be casted to the specific object
Yes It is possible in your custom adapter class in the getView() mtheod u can change imageview bitmap by clicking on it
see this code
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
View rowView = convertView;
rowView = inflater.inflate(R.layout.playlist_item, null);
final ImageView im = (ImageView) rowView.findViewById(R.id.favorite);
im.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
// Do your stuff here
}
});
return rowView;
}
ListViews ask Adapters for Views before they can be seen on screen. The default implementation of an Adapter actually inflates your views when the ListView asks for them.
When using an Object extends ArrayAdapter<MyObject>, we could actually store all the views in a HashMap<MyObject, SoftReference<View>> and override ArrayAdapter.getView so we can inflate our Views only once, but if the GarbageCollector passes and makes our Views null (or my SoftReferences are made null for any strange reason) we inflate them again.
#Override
public View getView(final int position, final View convertView,
final ViewGroup parent)
{
final MyObject mo = getItem(position);
if (hashMap.get(mo) == null || hashMap.get(mo).get() == null)
{
final MyCustomView mcv = new MyCustomView(getContext());
hashMap.put(mo, new SoftReference<MyObject>(mcv));
}
return hashMap.get(mo).get();
}
I've tried it and it works fine and nice. Is this implementation disencouraged in any way?
we could actually store all the views
You should use ViewHolder pattern. It allows you not to create all the views, but only those you need to display on the screen at the moment.
I am doing a project where i am a using a custom list view, which contains a textview. Data is coming from the server. I want to change the height of the cell, based on the data size. If the content is more than two lines, i have to trim it to two lines and show a see more button. On clicking the see more button expand the cell to show full details.I googled about it. But i cannot find any useful resource.Could any one help me to solve this, any useful links or suggestions?`
One way to do this is to first invalidate the listView forcing it to redraw the visible elements and then handle the layout change in the getView method of your adapter.
Try this:
Add a showTrimmed(int position, boolean value) method to your adapter. This will update a structure inside your adapter that keeps track of the list items that you want to trim or not trim. Then in the getView method, when creating the item view, you test if the current element should be trimmed or not and based on the result you create the proper view.
I did something similar to achieve a different but similar result and it works.
Remember to call invalidate after calling showTrimmed to force the listView to redraw the displayed items.
Edit: I post my code which is different from what you want to do but it's quite similar in the idea:
public class HeroListViewAdapter extends ArrayAdapter<Hero> {
public static int NO_POS = -1;
private List<Hero> heroes;
private int selected_pos = NO_POS;
public HeroListViewAdapter(Context context, List<Hero> heroes){
super(context, R.layout.hero_list_item_view, heroes);
this.heroes = heroes;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View rowView = GuiBuilder.createHeroListItemView(heroes.get(position),getContext(),parent,false);
if(position == selected_pos){
rowView.setBackgroundColor((rowView.getResources().getColor(R.color.list_item_selected_color)));
}
return rowView;
}
public void setSelectedPosition(int selected_pos){
this.selected_pos = selected_pos;
}
public int getSelectedPosition(){
return selected_pos;
}
public Hero getSelectedHero(){
if(selected_pos>=0 && selected_pos<heroes.size())
return this.getItem(selected_pos);
else
return null;
}
}
instead of setSelectedPosition you should have the showTrimmed method which updates an inner member as setSelectedPos does, in order to keep track of the trimmed state of every list item. Then in the getView method you do the test (like I do in if(position == selected_pos)) and then you build your custom trimmed or not trimmed list item based on the result of the test. My listener which uses these functions is:
listView.setOnItemClickListener(new AdapterView.OnItemClickListener(){
public void onItemClick(AdapterView<?> parent, View view, int position, long id){
list_adapter.setSelectedPosition(position);
listView.invalidate();
}
}
);
you can try to update this idea to your needs. In particular in my case I do this to highlight the clicked listview item by programmatically changing its background.