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;
}
Related
I'm working on implementing an ExpandableListView with my custom Adapter that extends BaseExpandableAdapter .
Working with the MVP model i successfully inflate all the groups and views in the right way. Piece of my GetChildView()
public override View GetChildView(int groupPosition, int childPosition, bool isLastChild, View convertView, ViewGroup parent)
{
BetViewHolder holder = new BetViewHolder(_BetPresenter);
var view = convertView;
if (view != null)
holder = view.Tag as BetViewHolder;
else
{
LayoutInflater layoutInflater = (LayoutInflater)_Context
.GetSystemService(Context.LayoutInflaterService);
holder = new BetViewHolder(_BetPresenter);
view = layoutInflater.Inflate(Resource.Layout.live_bet_view_item, null);
holder.InitEventViews(view);
view.Tag = holder;
}
_BetPresenter.OnBindBetEventView(holder, groupPosition, childPosition);
return view;
}
Inside the presenter at OnBindBetEventView i set the data on the corresponding view.
The problem
Each child item has a 3 custom components(Buttons) which are selectable. And in general each child view is relatively complex. When i select one of the buttons of the child view it successfully changes the color like it should, but also other child views from other groups change get selected too. The strange part comes when i scroll or collapse/expand a group.
The selected views are changing and some others are getting selected instead. 'Playing' with the groups it messes the all the selected buttons without a specific pattern.
Clicking the first button also checks another button on another group. It was not already selected by me. Scrolling and expanding groups changes the order of selected buttons
NOTE
After lots of debugging i realized that every 2 buttons the Unique view id's of the buttons are duplicate. Meaning that the instance of the view is exactly the same. Maybe is due to recycling or something. The point is that i need to preserve the state of my selected views without letting the adapted change the position of my views.
The recycling mechanism of the ExpandableList was causing this strange behavior due to the reuse-ability of the views. The ChildView with the clicked item was recycled once it was out of screen and inflating it again on a different position
I realize that due to the fact that the views had exactly the same unique hash created by Android. There was a duplication
I solved it by checking my model's state everytime the adapter tries to inflate the view, on the position it tries to inflate it.
Here is the ViewHolder method called from the adapter at GetChildView()
public void SetEventToView(Event eventItem)
{
txtCompetitor1.Text = eventItem.Competitor1;
txtCompetitor2.Text = eventItem.Competitor2;
txtTime.Text = eventItem.Time;
txtScore.Text = eventItem.Score;
for (int i = 0; i < _OddButtonList.Count; i++)
{
_OddButtonList[i].OddID = eventItem.BetItems[i].ID;
_OddButtonList[i].SetPriceText(eventItem.BetItems[i].BetOdd);
_OddButtonList[i].SetOnClickListener(this);
SetButtonState(eventItem, _OddButtonList[i], i);
}
}
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.
I'm trying to add some extra space between the 4th and 5th items in the listview. What are my options?
I tried doing that in adapter's getView(), as well as manually getting access to the fourth element and adding padding to it.
Is there a better way to do this?
Another way to do this would be to use a different layout for the the 4th item (that has additional padding). It's similar to your solution but maybe a bit "cleaner". I'm assuming that you're extending ArrayAdapter.
In your adapter override the getViewTypeCount() method:
#Override public int getViewTypeCount() {
return 2;
}
This way you're telling your adapter that you will use two different layouts for your items. Next, you have to specify which items will be of which type by overriding another method:
#Override public int getItemViewType(int position) {
if(position == 3) {
return 0;
} else {
return 1;
}
}
This will tell your adapter to use a different view (only) for the 4th element in the list, and it will not be reused for other elements. Now for the last part, override onCreateView():
#Override public View getView(int position, View convertView, ViewGroup parent) {
if(convertView == null) {
LayoutInflater inflater = LayoutInflater.from(context);
if(position == 3) {
convertView = inflater.inflate(R.layout.your_layout_with_padding, parent, false);
} else {
convertView = inflater.inflate(R.layout.your_regular_item_padding, parent, false);
}
//TODO this is the place to initialize your view holder
} else {
//TODO this is the place to restore your view holder
}
//TODO setup your view here
return convertView;
}
For the item with position == 3 (4th item in the list) convertView argument of the getView() method will be null, because that is the first (and only) item of the type 1 in the list. Therefore you can inflate a different layout that includes a padding for that item.
I thought to some ways but if i have to be honest the only way to do this well is to change the layout in the adapter when the position is equal to 4. I meant that you can do an xml file with a RelativeLayout of the height that you want as space between the 4th and 5th element and set the visibility to gone and put him above all your adapter's elements. When the position is equal to 4 in your getView you set the visibility of that item to visible with nameOfYourRelativeLayout.setVisibility(View.VISIBLE)
So you can add this blank space only between 4th and 5th element. Mine is just a suggestion but i think it can work well.
Layout all of your items in the listview to include your data as well as a header view, maybe a textview or even a Viewgroup like another layout. Keep the header invisible until some logic in your code triggers (i.e. pos ==4) and make the header visible
I have an ExpandableListView that holds certain objects. These objects store a property that determines the background of the view which they are held in. My question is, how do I make sure that these backgrounds are always correct when android recycles views? The issue that I am running into is that when I change this background determining property on a certain row and the xml resource changes on that child row, the next item created for the list uses that same view even though it may not be correct. Here is my code below of my custom ExpandableListViewAdapter that determines the xml resource for the view of the ExpandableListView child row.
#Override
public View getChildView(int groupPosition, int childPosition,
boolean isLastChild, View convertView, ViewGroup parent) {
ToDoItem item = mParent.get(groupPosition).getParentChildren().get(childPosition);
view = convertView;
if(view == null) {
// Check item priority and set background accordingly
if(item.getPriority() == 2) {
view = inflater.inflate(R.layout.list_item_medium, null);
} else if(item.getPriority() == 3) {
view = inflater.inflate(R.layout.list_item_high, null);
} else if(item.getPriority() == 1) {
view = inflater.inflate(R.layout.list_item, null);
}
}
TextView tv = null;
if (view != null) {
tv = (TextView) view.findViewById(R.id.tasktext);
tv.setText(item.getTask());
}
return view;
}
As you can see I am checking if the view is equal to null to make sure my app runs at an acceptable speed. How can I keep my optimization while maintaining the functionality of the backgrounds?
The issue that I am running into is that when I change this background determining property on a certain row and the background changes on that row, the next item created for the list uses that same background even though it may not be correct.
Presumably, that is because you have not implemented getViewTypeCount() and getItemViewType(), to teach the AdapterView what object pools to use for caching and recycling purposes. If you will have different row structures based on position, you need to properly implement those two methods.
Also, be very careful about modifying list row backgrounds so that you don't interfere with the touch feedback on taps or the selection highlight bar when used with a pointing device (D-pad, trackball, arrow keys, etc.). I would recommend you do just about anything else to distinguish the rows other than manipulating the background.
Since you apparently are using ExpandableListView, not ListView, there are up to four different methods that you need to override on your subclass of BaseExpandableListAdapter.
Two are getGroupTypeCount() and getChildTypeCount(). These return the number of distinct row types you are using at the group level and at the child level, respectively. By "distinct row types", I mean "situations where the rows differ enough that your concerns about recycling come into play". Based on the snippet of code in your answer, we can see that you have three distinct child row types, as you are inflating three different layouts based upon priority. Hence, you would have:
#Override
public int getChildTypeCount() {
return(3);
}
If you also have different row structures at the group level, you would need to override getGroupTypeCount() in a similar fashion.
Then, there are getGroupType() and getChildType(). These return a value, from 0 to getGroupTypeCount()-1 and getChildTypeCount()-1, indicating which recycling bucket should be used for a given groupPosition (or groupPosition and childPosition for getChildType()). If you override get...TypeCount(), you must also override the corresponding get...Type() method.
So, we know we need getChildType(). Given your existing code above, that implementation would look like:
#Override
public View getChildView(int groupPosition, int childPosition) {
ToDoItem item = mParent.get(groupPosition).getParentChildren().get(childPosition);
return(item.getPriority()-1);
}
Since you happen to have getPriority() running from 1 to 3, we can just subtract 1 from that to get a value in our desired 0 to 2 range. If you were basing the row layouts on something else (e.g., third character in the title of the to-do item), you'd need to use that logic in getChildView() instead.
And, if you override getGroupTypeCount(), you'd also override getGroupType().