Taking for example Gmail App, on my Navigation Drawer, I want a ListView that is grouped by section, similar to inbox, all labels.
Is this behavior achieved by using multiple ListView separated by a "header" TextView (which I have to build manually obviously), or is this section-grouped behavior supported by the Adapter or ListView?
Don't use multiple ListViews, it will mess things up for the scroll.
What you describe can be achieve by using only one ListView + adapter with multiple item view types like this:
public class MyAdapter extends ArrayAdapter<Object> {
// It's very important that the first item have a value of 0.
// If not, the adapter won't work properly (I didn't figure out why yet)
private int TYPE_SEPARATOR = 0;
private int TYPE_DATA = 1;
class Separator {
String title;
}
public MyAdapter(Context context, int resource) {
super(context, resource);
}
#Override
public boolean areAllItemsEnabled() {
return false;
}
#Override
public int getItemViewType(int position) {
if (getItem(position).getClass().isAssignableFrom(Separator.class)) {
return TYPE_SEPARATOR;
}
return TYPE_DATA;
}
#Override
public int getViewTypeCount() {
// Assuming you have only 2 view types
return 2;
}
#Override
public boolean isEnabled(int position) {
// Mark separators as not enabled. That way, the onclick and onlongclik listener
// won't be triggered for those items.
return getItemViewType(position) != TYPE_SEPARATOR;
}
}
You just have to implement your own getView method for a correct rendering.
I am not sure exactly how the Gmail app achieves this behavior, but it seems as though you should work on a custom adapter. Using multiple list views would not be a productive way to approach this problem, as one wants to keep the rows of data (messages) together in single list items.
Related
I have a GridView layout that makes use of an ArrayAdapter to populate its contents. I want to make use of fast-scrolling and as such have added the following attributed to the layout XML:
android:fastScrollAlwaysVisible="true"
android:fastScrollStyle="#android:style/Widget.Material.FastScroll"
I am now able to make use of fast scrolling to navigate but would now like to add a material thumb preview as such:
From my understanding, I would have to implement the SectionIndexer interface from my ArrayAdapter as so:
class exampleArrayAdapter extends ArrayAdapter<...> implements SectionIndexer
At this point, I have reached a bump and can't figure out how to get the thumb preview and fear I may be doing something wrong. Pointers as to how I can get this working or what I should look up would be appreciated.
I have finally had time to look back at this, and the solution turns out to be very trivial! This is what I did:
#Override
public Object[] getSections() {
ArrayList<String> labels = new ArrayList<>();
for (LaunchableActivity activity: mActivityInfos) {
labels.add(activity.getActivityLabel());
}
return labels.toArray();
}
#Override
public int getPositionForSection(int i) {
return i;
}
#Override
public int getSectionForPosition(int i) {
// We do not need this
return 0;
}
I had a list of LaunchableActivity and based of that created a sections array to be returned. For my needs, all I required was to implement getPositionForSection and not getSectionForPosition. Your use case may vary.
The source code where I implemented this is available here, specifically on commits:
a2c9ddd1c647919afbf24262ac1a7772a08e468c
08802e17f4c75835c28232353fed68964f5d7746
0d73a788c6b92d7b9c05a2871778da42af02afd8
f13a59f02690481801bd07ffc593648b8e71d036
I know there are lots of threads already on this topic, but none of the given solutions worked for me so far. I'm trying to add or update an item of a RecyclerView. Here's my code so far:
MainActivity
private MyListItemAdapter mAdapter;
private RecyclerView recyclerView;
// called on activity create
private void init() {
// initialize activity, load items, etc ...
mAdapter = new MyListItemAdapter(this, items);
recyclerView.setAdapter(mAdapter);
}
// called when I want to replace an item
private void updateItem(final Item newItem, final int pos) {
mAdapter.replaceItem(newItem, pos);
}
MyListItemAdapter
public class MyListItemAdapter extends RecyclerView.Adapter<MyListItemAdapter.MyListItemViewHolder> {
private List<Item> mItems;
public void replaceItem(final Item newItem, final int pos) {
mItems.remove(position);
mItems.add(position, newItem);
notifyItemChanged(position);
notifyDataSetChanged();
}
}
I tried to make this changes from the MainActivity aswell, but in every case I tried my list doesn't get updated. The only way it worked was when I reset the adapter to the recyclerView:
mAdapter.notifyDataSetChanged();
recyclerView.setAdapter(mAdapter);
which obviously is a bad idea. (aside from the bad side effects wouldn't even work when I'm using lazy loading on my lists).
So my question is, how can I make notifyDataSetChanged() work properly?
edit
I found a solution for replacing items. After mAdapter.replaceItem(newItem, pos); I had to call recyclerView.removeViewAt(position);
This works for replacing an item, but doesn't solve my problem when I want to add items (e.g. lazy loading) to my list
edit2
I found a working solution for adding items
Adapter:
public void addItem(final Item newItem) {
mItems.add(newItem);
notifyDataSetChanged();
}
Activity:
private void addItem(final Item newItem) {
mAdapter.addItem(newItem);
recyclerView.removeViewAt(0); // without this line nothing happens
}
For some reason this works (also: it doesn't remove the view at position 0), but I'm sure this isn't the correct way to add items to a recyclerView
This should work:
private ArrayList<Item> mItems;
public void replaceItem(final Item newItem, final int position) {
mItems.set(position, newItem);
notifyItemChanged(position);
}
ArrayList.set() is the way to go to replace items.
For adding items, just append them to mItems and then go notifyDatasetChanged(). Another way to go is to use notifyItemRangeInserted(). Depending on where/how are you adding new items and how many of them, it might be worth it.
Use
mItems.set(position, newItem);
instead of
mItems.add(position, newItem);
because .set method will replace your data to particular position.
I have a SortedList being displayed in a RecyclerView by my RecyclerView.Adapter.
I use 2 custom Comparator instances from withing the SortedListAdapterCallback.compare() method to either sort A-Z or Z-A.
static class A2Z implements Comparator<Item> {
#Override
public int compare(Item t0, Item t1) {
return t0.mText.compareTo(t1.mText);
}
}
static class Z2A extends A2Z {
#Override
public int compare(Item t0, Item t1) {
return -1 * super.compare(t0, t1);
}
}
Item simply contains a single String mText;
I use my comparators in the SortedListAdapterCallback.compare() method:
private Comparator<Item> a2z = new A2Z();
private Comparator<Item> z2a = new Z2A();
private Comparator<Item> comparator = z2a;
#Override
public int compare(Item t0, Item t1) {
return comparator.compare(t0, t1);
}
I change the comparators on a button press. The list on screen does not update.
After logging values in the various methods, I can tell that the list itself is not updating. Notifying the adapter of changes simply redraws the old list, without resorting it.
So how do I force the underlying SortedList to resort all the items?
Perhaps it is best to just create a new Adapter each time, as in this question:
RecyclerView change data set
SortedList does not have functionality to resort itself - each instance only has a single sort order.
Went with creating a new adapter for each resort, as per Yigit's answer to the above referenced question:
If you have stable ids in your adapter, you can get pretty good
results (animations) if you create a new array containing the filtered
items and call
recyclerView.swapAdapter(newAdapter, false);
Using swapAdapter hints RecyclerView that it can re-use view holders.
(vs in setAdapter, it has to recycle all views and re-create because
it does not know that the new adapter has the same ViewHolder set with
the old adapter).
Use a switch statement inside the compare method with a local control flag (an enum is a good idea).
After changing the switch flag, call sortedList.replaceAll.
#Override
public int compare(PmpRole pmpRoleA, PmpRole pmpRoleB) {
switch (mSorter){
case IDX:
return pmpRoleA.getIdx().compareTo(pmpRoleB.getIdx());
case TITLE:
return pmpRoleA.getTitleIdx().compareTo(pmpRoleB.getTitleIdx());
case ID_IDX:
return pmpRoleA.getIdIdx().compareTo(pmpRoleB.getIdIdx());
}
return -1;
}
public void setSorter(Sorter sorter){
mSorter = sorter;
mPmpRoleSortedList.replaceAll(mPmpRoles);
}
Maintains animation functionality etc.
I want to know if is it a good way to use the same adapter for more than one listview.
in my code i have many listviews and each one contains the same UL components like imageview and textview, so is it good to use `MyAdapter extends BaseAdapter` for each of them ? or it is better to make adapter for each one?
if i have to use one adapter, how to handle the different onclick actions for the button, imageview and textview for each listview ?
class MyAdapter extends BaseAdapter {
public MyAdapter() {
}
#Override
public int getCount() {
// TODO Auto-generated method stub
return data.size();
}
#Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return position;
}
#Override
public long getItemId(int position) {
// TODO Auto-generated method stub
return position;
}
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
return null;
}
}
It will make no difference resource-wise either way, as you will have to create a new instance of the adapter for each listview anyway. But trying to incorporate the features of two different adapters into one even just sounds overly complex. I would say for clarity of design, just make two different adapters. It'll make your life so much easier in the long run when it comes to debugging as well.
Keep in mind this is when the behaviors of each list are different, if the lists are supposed to function the same go ahead and use the same adapter for each.
Are you talking about reusing the instance of the adapter or its class? The class can be reused ad infinatum.
The instance, however, is safer not to be reused. The reason for this is you will likely have collsions or artifacts from the previous AdapterView. Adapter creation is menial, so why not just be safe and create a new one for each AdapterView?
This is a really good question I often struggle with. Seems so unnecessary duplicating so much adapter code just for different actions. I still struggle with this questions as a design issue, so my answer is not intended to provide an answer on that. However, for the part of the question about reusing the adapter or not, what I do if I wish to reuse a list/adapter is this:
For each type of list I create a global constant value to act as an identifier for that type of list. When I create a new instance of the adapter I supply the requestId/listTypeId to the adapter:
//first i create the constants somewhere globally
TYPE_ID_A = 0;
TYPE_ID_B = 1;
TYPE_ID_C = 2
//then i feed them to my adapter and set the clickListener on my list
mList.setAdapter(new MyListAdapter(mContext, listData, TYPE_ID_A));
mList.setOnItemClickListener(this);
In my adapter I set this typeId as a member variable and further then create a public function to return this id:
public class MyListAdapter extends ArrayAdapter<JSONArray> {
private final Context mContext;
private final JSONArray mItems;
private final int mListType;
//assign the values in the constructor of the adapter
public SearchListAdapter(Context context, JSONArray items, int listType) {
super(context, R.layout.item_filter_list);
mItems = items;
mContext = context;
mListType = listType;
}
//function to return the list id
public int getListType(){
return mListType;
}
}
Finally, inside my onClick listener I call this function inside my adapter to return the listTypeId which I then compare the global constants to identify what do to further:
#Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
MyListAdapter adapter = (MyListAdapter) adapterView.getAdapter();
int listType = adapter.getListType(); //get the listTypeId now
//now see which list type was clicked:
switch(listType){
case(TYPE_ID_A):
//to action for list A
break;
case(TYPE_ID_B):
//to action for list B
break;
}
}
This works for me but I dont think its great. If any one has another proper design pattern please let us know!
I have a class which implements expandable list activity.
In the XML code (or I can do it in java), I set fastScrollEnabled to true. This does in deed enable fast scroll. BUT fast scroll only works in the top portion of the list. Like I can use the fastscroll thumb bar to scroll the whole list but only works in the top section of the scroll bar. It's not proportionate to the entire list. I can drag the thumb bar to the bottom of the list but it does no scrolling since the listview is already scrolled to the bottom due to the odd behaviour of it only working in the top portion of the list.
Confusing I know, I can try to clarify more if needed....
I do implement a custom BaseExpandableListAdapter.
I've just found a workaround to prevent the system to display this wrong behaviour.
There are two scenarios which use different code for the SectionIndexer to work.
The first scenario is the case that you use the FastScrollbar-Thumb to navigate to the next section. Assuming that the groups are your sections the overriden methods for implementing the SectionIndexer would look like that:
#Override
public int getPositionForSection(int section) {
return section;
}
// Gets called when scrolling the list manually
#Override
public int getSectionForPosition(int position) {
return ExpandableListView.getPackedPositionGroup(
expandableListView
.getExpandableListPosition(position));
}
The second scenario is the case that you scroll the list manually and the fast scrollbars move according to the sections, not to all items. The code therefore looks like that:
#Override
public int getPositionForSection(int section) {
return expandableListView.getFlatListPosition(
ExpandableListView.getPackedPositionForGroup(section));
}
// Gets called when scrolling the list manually
#Override
public int getSectionForPosition(int position) {
return ExpandableListView.getPackedPositionGroup(
expandableListView
.getExpandableListPosition(position));
}
As one can see these two behaviours can not play together without further adoption.
The workaround to make it both work is to catch the case when someone is scrolling per hand (i.e. scrolling via touch). This can be done with implementing the OnScrollListener interface with the adapter class and set it onto the ExpandableListView:
public class MyExpandableListAdapter extends BaseExpandableListAdapter
implements SectionIndexer, AbsListView.OnScrollListener {
// Your fields here
// ...
private final ExpandableListView expandableListView;
private boolean manualScroll;
public MyExpandableListAdapter(ExpandableListView expandableListView
/* Your other arguments */) {
this.expandableListView = expandableListView;
this.expandableListView.setOnScrollListener(this);
// Other initializations
}
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
this.manualScroll = scrollState == SCROLL_STATE_TOUCH_SCROLL;
}
#Override
public void onScroll(AbsListView view,
int firstVisibleItem,
int visibleItemCount,
int totalItemCount) {}
#Override
public int getPositionForSection(int section) {
if (manualScroll) {
return section;
} else {
return expandableListView.getFlatListPosition(
ExpandableListView.getPackedPositionForGroup(section));
}
}
// Gets called when scrolling the list manually
#Override
public int getSectionForPosition(int position) {
return ExpandableListView.getPackedPositionGroup(
expandableListView
.getExpandableListPosition(position));
}
// Your other methods
// ...
}
That fixed the bug for me.
This is a bug in the fast scroller. It does not play well with ExpandableListView.
See my code.
(it also includes a work-around for some cases)