I've seen quite a few examples of how to add alphabetical section headers to list views online. Example:
I implemented the functionality from
this website
. However, I have a list of approximately 8000 items. When trying to load this page it takes about 8 seconds which is obviously way too slow. With just a normal AlphabetIndexer it takes about 1.5 seconds (still slow, but much better).
Does anyone have any ideas on how to speed this up? If not, are there any other examples that are quicker than this?
Thanks!
What exactly do you mean by trying to load the page? How long does it take if you just load the items and don't do any indexing? 8000 items is not that many to iterate over. It could however be a lot of items to load from disk, or the internet. You might want to consider showing a loading screen and reading in the data for your rows in the background.
The code you showed looks particularly complicated for what you're trying to do. Below is a solution I've used. You can google for SectionIndexer. In my code itemManager is basically just an abstraction on a list, placeholders are null values, everything else is the data structure containing the information for the rows. Some code is omitted:
//based on http://twistbyte.com/tutorial/android-listview-with-fast-scroll-and-section-index
private class ContactListAdapter extends BaseAdapter implements SectionIndexer {
final HashMap<String, Integer> alphaIndexer = new HashMap<String, Integer>();
final HashMap<Integer, String> positionIndexer = new HashMap<Integer, String>();
String[] sections;
public ContactListAdapter() {
setupHeaders();
}
public void setupHeaders(){
itemManager.clearPlaceholders();
for (int i = 0; i < itemManager.size(); i++) {
String name = itemManager.get(i).displayName();
String firstLetter = name.substring(0, 1);
if (!alphaIndexer.containsKey(firstLetter)) {
itemManager.putPlaceholder(i);
alphaIndexer.put(firstLetter, i);
positionIndexer.put(i, firstLetter);
++i;
}
}
final Set<String> sectionLetters = alphaIndexer.keySet();
final ArrayList<String> sectionList = new ArrayList<String>(sectionLetters);
Collections.sort(sectionList);
sections = new String[sectionList.size()];
sectionList.toArray(sections);
}
#Override
public int getItemViewType(int position) {
return itemManager.isPlaceholder(position) ? ViewType.HEADER.ordinal() : ViewType.CONTACT.ordinal();
}
#Override
public int getViewTypeCount() {
return ViewType.values().length;
}
#Override
public int getPositionForSection(int section) {
return alphaIndexer.get(sections[section]);
}
#Override
public int getSectionForPosition(int position) {
return 1;
}
#Override
public Object[] getSections() {
return sections;
}
ListView adapters have a class you can override called getViewType(int position) and getViewTypeCount().
Those you would override to allow Android to know how many different type of views your adapter uses.
In your case override getViewTypeCount() to return 2 since you will have two types of views. One your standard and second your header view.
Than you will need to know at what position you will have your header views shown. Once you do you can easily return your views on the getView(...) function.
Related
In my project I have class that extends ArrayAdapter<String> and implements SectionIndexer. When implementing methods getPositionForSection and getSectionForPosition I have found some strange behaviour.
Why section indexer works properly when getSectionForPosition returns 0?
public int getSectionForPosition(int position) {
return 0;
}
this implementation is used in many tutorials, for example:
http://androidopentutorials.com/android-listview-fastscroll/
http://www.survivingwithandroid.com/2012/12/android-listview-sectionindexer-fastscroll.html
Documentation says that
Given a position within the adapter, returns the index of the
corresponding section within the array of section objects.
so if my list have 5 items starting with letter "A" and some items starting with letter "B", then getSectionForPosition(5) should return 1.
While the basic functionality of the sectionIndexer (scrolling through the section indices when pulling the scrollbar thumb) is unaffected, returning a constant in the method getSectionForPosition can lead to a misbehaviour of the scrollbar positioning when swiping through the list (e.g. the scrollbar moving out of the window on the y-axis).
Apparently this misbehaviour only occurs when the list or single sections exceed a certain length, so for shorter lists the indexer might seem to work correctly (but in fact doesn't). I experienced this misbehaviour in larger lists when returning a constant in the above method several times and fixed it by implementing getSectionForPosition in a correct way.
However, since it might be interesting for others I can provide a sample implementation of that method. Let azIndexer be a HashMap containing a correct section -> position mapping of my indices and listItems be the items arranged by the adapter. The following builds a position -> sectionIndex mapping stored in positionIndexer.
List<Integer> values = new ArrayList();
for (int i : azIndexer.values()) {
values.add(i);
}
values.add(listItems.size()-1);
Collections.sort(values);
int k = 0;
int z = 0;
for(int i = 0; i < values.size()-1; i++) {
int temp = values.get(i+1);
do {
positionIndexer.put(k, z);
k++;
} while(k < temp);
z++;
}
which will then be used in the getSectionForPosition method:
#Override
public int getSectionForPosition(int position) {
return positionIndexer.get(position);
}
Your callback method getPositionForSection(int section) needs to be implemented to give the right position which is used by setSelection to return the right position where you want to scroll on touch of SectionIndexer on rightside.
Make sure you are doing this
this.setSelection(((SectionIndexer) getAdapter()) .getPositionForSection(currentPosition));
You need to implement two callback methods
#Override
public Object[] getSections() {
Log.d("ListView", "Get sections");
String[] sectionsArr = new String[sections.length()];
for (int i=0; i < sections.length(); i++)
sectionsArr[i] = "" + sections.charAt(i);
return sectionsArr;
}
This final callback and you are done here
#Override
public int getPositionForSection(int section){
Log.d("ListView", "Get position for section");
for (int i=0; i < this.getCount(); i++) {
String item = this.getItem(i).toLowerCase();
if (item.charAt(0) == sections.charAt(section))
return i;
}
return 0;
}
I have a class ShoppingList with the following Map:
#DatabaseField(dataType = DataType.SERIALIZABLE)
Map<Category, List<Product>> products;
Then, on a Fragment, I have a ListView that should show all Categories. Therefore, I set the Adapter for it with the following code:
aisles.setAdapter(new ShoppingListEditAdapter(getActivity(), new ArrayList<Category>(mShoppingList.getMap().keySet())));
By default it should be empty. If I click a button, a Dialog opens, and there I input the text for the new category or aisle that I want to create. Thus, I run the following code:
Category category = new Category();
category.setName(editText.getText().toString());
mShoppingList.getMap().put(category, new ArrayList<Product>());
try {
helper.getShoppingListDao().update(mShoppingList);
((BaseAdapter) aisles.getAdapter()).notifyDataSetChanged();
} catch (SQLException e) {
e.printStackTrace();
}
This should modify the ShoppingList I have for this Fragment, adding a Category to the Map. However, even if I call notifyDatasetChanged() for my Adapter or restart the application, the ListView doesn't get populated with the data. Both Category and Product are Serializable. Why doesn't it get populated?
In keeping with what I've posted as a comment, here is a simple adapter that will accept a Map and display the keys of the map as the values of the list. I've not tested it in accordance with the actual item id's, but for displaying a map that will be mutated, it seems to work (i.e. if the map itself is changed and notifyDataSetChanged() is called, it will work). I've counted on ArrayAdapter to do the heavy lifting and just overrode the methods I wanted.
The obvious downside is I'm not sure how expensive it is to call keySet().toArray() but I imagine it's a linear time operation, beyond the extra memory usage. But basically I don't know a great way to get around this issue, as it relies on taking a Set into some sort of ordered collection.
public class MapAdapter<K, V> extends ArrayAdapter<K> {
private Map<K, V> items;
public MapAdapter(Context context, int resource, Map<K,V> items) {
super(context, resource);
this.items = items;
}
#Override
public int getCount() {
return items.size();
}
#Override
public K getItem(int position) {
return ((K []) items.keySet().toArray())[position];
}
}
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.
I am implementing ListView with section in which I show custom section headers apart from the conventional alphabets as the header. In order for me to implement the custom SectionIndexer correctly, I wish to understand the difference between the two methods getSectionForPosition and getPositionForSection.
I understand (not sure if that's correct) that getSectionForPosition returns the alphabet we want to show in the section header.
I don't understand the other method. Also, are they similar in any sense (if at all) and in what way they differ (if they do, which I think they do :) )
Anybody having understanding about this may kindly post an answer to this. Appreciate your time going through the question.
Update:
I have gone through the documentation at this official page; I'm looking for some elaborated insight with respect to custom SectionIndex implementation
getPositionForSection(section) returns the first position at which the cursor data at the indexed column starts with the section.
For example if the index of section B is 1 and the indexed column of the cursor has the following data
Position Data getSectionForPosition(position)
_________ __________ ______________________________
0 Abhfdf 0
1 Achahtkh 0
2 Ahtjlarej 0
3 Bchatkd 1
4 Bjklhdsfoi 1
5 Bzhafdlsfk 1
6 Cj fadsfkj 2
then getPositionForSection(1) returns 3
also getPositionForSection(2) returns 6
Hope this helps you
Here's a sample implementation I created. Perhaps it clarifies the SectionIndexer a bit.
For holding your data, you could use private inner classes:
private class Item {
public int sectionNumber;
public Long id;
public String name;
}
private class Section {
public int startPosition;
public String header;
#Override
public String toString(){
return header;
}
}
Use a list for your items and one for your sections:
private List<Item> items = new ArrayList<Item>();
private List<Section> sections = new ArrayList<Section>();
The implementation in your list adapter for SectionIndexer could be like the following now:
#Override
public int getPositionForSection(int sectionNumber) {
return sections.get(sectionNumber).startPosition;
}
#Override
public int getSectionForPosition(int itemPosition) {
return items.get(itemPosition).sectionNumber;
}
#Override
public Object[] getSections() {
return sections.toArray();
}
Ofcourse you should implement a method now to fill your adapter's data lists.
The method getPositionForSection(int section) returns you the starting index in list for section
while the method getSectionForPosition(int position) returns you the index of section which contains item at position
I am following a great coding example over here: This SO question. It is regarding implementing a SectionIndexer interface to an array adapter.
However, how would you do the same thing if your ArrayAdapter is passing an ArrayList< MyObject > not an ArrayList< String >?
For example, this is where my code is different then his code. He has:
class AlphabeticalAdapter extends ArrayAdapter<String> implements SectionIndexer {
private HashMap<String, Integer> alphaIndexer;
private String[] sections;
public AlphabeticalAdapter(Context c, int resource, List<String> data) {
alphaIndexer = new HashMap<String, Integer>();
for (int i = 0; i < data.size(); i++) {
String s = data.get(i).substring(0, 1).toUpperCase();
alphaIndexer.put(s, i);
}
// other stuff
}
I am having problems adapting that for loop to my situation. I can't measure the size like he does. Where he has the above, my adapter begins with.
public class CustomAdapter extends ArrayAdapter<Items> implements
SectionIndexer {
public ItemAdapter(Context context, Items[] objects) {
Where he is passing one ArrayList, I have to pass in three, but to make that happen, had to wrap in a custom object class. One of the ArrayLists that I want to sort is one of three fields in the class called "name". It is a string obviously.
I want to scroll through that alphabetically with SectionIndex based on that name field. How do I change the example code from the other question to work in this scenario?
Where he has "data.size()", I need something like "name.size()" - I think?
Where he is passing one ArrayList, I have to pass in three, but to
make that happen, had to wrap in a custom object class. One of the
ArrayLists that I want to sort is one of three fields in the class
called "name".
You don't have three ArrayLists, you have an ArrayList of custom objects that were built from three ArrayLists(so the size is the size of the List that you pass to the adapter). From this point of view the only change in your code is to use the name from that custom object Items to build the sections:
for (int i = 0; i < data.size(); i++) {
String s = data.get(i).name.substring(0, 1).toUpperCase();
if (!alphaIndexer.containsKey(s)) {
alphaIndexer.put(s, i);
}
}
// ...
There aren't other changes. Also you may need to sort the List of Items that you pass to the adapter using:
Collections.sort(mData);
where your Items class must implement the Comparable<Items> interface:
class Items implements Comparable<Items> {
String name;
// ... rest of the code
#Override
public int compareTo(Items another) {
// I assume that you want to sort the data after the name field of the Items class
return name.compareToIgnoreCase(another.name);
}
}