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
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 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.
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'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.
Is there any way to re-index a SectionIndexer after new items are added to a ListView?
I found this solution, but the overlay is position in the top left corner after the SectionIndexer is refreshed.
Anyone have any ideas?
Once the FastScroller (its in AbsListView class that ListView extends from) obtains your sections by calling SectionIndexer#getSections(), it never re-obtains them unless you enable/disable fast-scrolling like mentioned in the link you mentioned. To get the value to be displayed on screen, FastScroller calls the section's toString method.
One potential solution is to have a custom SectionIndexer that have the following characteristics:
The sections array is of fixed length (max length of the expected number of sections. For example, if the sections represent English alphabet it will be 26)
Have a custom object to represent sections, rather than using strings
Overwrite the toString method of your custom section object to display what you want based on the current 'section values'.
-
e.g. In your custom SectionIndexer
private int mLastPosition;
public int getPositionForSection(int sectionIndex) {
if (sectionIndex < 0) sectionIndex = 0;
// myCurrentSectionLength is the number of sections you want to have after
// re-indexing the items in your ListView
// NOTE: myCurrentSectionLength must be less than getSections().length
if (sectionIndex >= myCurrentSectionLength) sectionIndex = myCurrentSectionLength - 1;
int position = 0;
// --- your logic to find the position goes in here
// --- e.g. see the AlphabeticIndexer source in Android repo for an example
mLastPosition = position;
return mLastPosition;
}
public Object[] getSections() {
// Assume you only have at most 3 section for this example
return new MySection[]{new MySection(), new MySection(), new MySection()};
}
// inner class within your CustomSectionIndexer
public class MySection {
MySection() {}
public String toString() {
// Get the value to displayed based on mLastPosition and the list item within that position
return "some value";
}
}
I found that the best way to do this is to call setContentView(R.layout.whatever) and then re-populate the ListView with your new adapter / new data items. This will redraw the ListView with your new items and the FastScroll Overlay will appear in the correct place.
I found notifyDataSetInvalidated working fine, here's the idea:
public class MyAdapter extends XXXAdapter implements SectionIndexer {
...
public void updateDataAndIndex(List data, Map index) {
// update sections
// update date set
notifyDataSetInvalidated();
}
}
update your data set and index (sections) somehow, and then notifyDataSetInvalidated, the index will refresh.
You can force reloading sections list to ListView by listView.setAdapter(yourAdapter)