This tutorial describes how to use Android ListView SectionIndexer to enable fast search. Android ListView has a method that enables the fast scroll called setFastScrollEnabled. To enable Android ListView SectionIndexer, we have to pass a true value to our Android Adapter and it must implement SectionIndexer. I want to create a custom component using Android ListView SectionIndexer so that we can move fast along the ListView items selected by the first letter.
In other words, we would like to obtain something as shown in the picture below:
Otherwise when a user moves his finger outside the scroll bar the listview scrolls as always.
So let’s start.
The first thing we need to create a custom component derived from android.widget.ListView, we call it FastSearchListView. This component behaves like a “normal” ListView and adds on the right side the scroll bar as shown in the picture above. The code by now is very simple:
[java]public class FastSearchListView extends ListView {private Context ctx;
private static int indWidth = 20;
private String[] sections;
private float scaledWidth;
private float sx;
private int indexSize;
private String section;
public FastSearchListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
ctx = context;
}
public FastSearchListView(Context context, AttributeSet attrs) {
super(context, attrs);
ctx = context;
}
public FastSearchListView(Context context, String keyList) {
super(context);
ctx = context;
}
…..
}[/java]
Now we have to override the onDraw method to change the ListView standard component behavior. By now we can suppose that we have an array of strings representing the alphabet letters from a…z. So the onDraw method looks like:
[java]@Overrideprotected void onDraw(Canvas canvas) {
super.onDraw(canvas);
scaledWidth = indWidth * getSizeInPixel(ctx);
sx = this.getWidth() – this.getPaddingRight() – scaledWidth;
Paint p = new Paint();
p.setColor(Color.WHITE);
p.setAlpha(100);
canvas.drawRect(sx, this.getPaddingTop(), sx + scaledWidth,
this.getHeight() – this.getPaddingBottom(), p);
indexSize = (this.getHeight() – this.getPaddingTop() – getPaddingBottom())
/ sections.length;
Paint textPaint = new Paint();
textPaint.setColor(Color.DKGRAY);
textPaint.setTextSize(scaledWidth / 2);
for (int i = 0; i < sections.length; i++)
canvas.drawText(sections[i].toUpperCase(),
sx + textPaint.getTextSize() / 2, getPaddingTop()
+ indexSize * (i + 1), textPaint);
}
}[/java]
What we do here it is quite simple. In the first lines, we calculate the real width of the scroll bar and the starting point along x-axis. Then we draw a rectangle (canvas.drawRect) with a semi-transparent background. The next step we need to get the indexSize, it means how big it is the space between each letter. The last step is to write the letters inside the rectangle (scrollbar). To make this example working you need to create and adapter i called SimpleAdapter. I have derived this adapter from ArrayAdapter. If you want more information about it you can refer to this post.
Running this example you have:
Android Listview SectionIndexer and adapter
As we told before we have a custom adapter derived from ArrayAdapter and we need a way to handle the fast scroll. As the android documentation says we need to implements a SectionIndexer interface.Our custom adapter then must implement this interface. Without digging into the details of our custom adapter we can focus our attention on the methods required by this interface. Let’s suppose that sections are defined as
[java] <pre>private static String sections = “abcdefghilmnopqrstuvz”;[/java]
then we have
[java]….@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;
}
@Override
public int getSectionForPosition(int arg0) {
Log.d(“ListView”, “Get section”);
return 0;
}
@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;
}
The first method getPositionForSection simply retrieves the position inside the listView given a section index. In our case, it is quite simple because to know the listview position we have to find the first item that starts with the letter corresponding to the section index. If we suppose that our section index string is a simple string from a to z to obtain the letter we can simply use getCharAt. Then we iterate over the listview items and find the first one starting with this letter.
The other method getSectionForPosition is not implemented because we don’t use it.
The last one getSections simply converts our string index into an array of objects. We will use this method in our custom listview. In the custom component when we set the adapter, we simply create a string array with all sections.
[java]@Overridepublic void setAdapter(ListAdapter adapter) {
super.setAdapter(adapter);
if (adapter instanceof SectionIndexer)
sections = (String[]) ((SectionIndexer) adapter).getSections();
}
[/java]
Handle Touch Event Inside and Outside the Scrollbar
Now we have to handle the touch events so that when the user touches outside the scrollbar area our custom component behaves like a normal list view and when a user touches inside the scroll bar are we have to handle this “touch” in another way. To know it we can simply retrieve the x coord touch position and compares it against sx value (see the code above). If x is greater than sx then we touch the scroll bar otherwise we touched the list. To know which letter we touched we have to know the y coord touch position and divide it with the indexSize. After we know the index inside the sections using that simple conversion, we use the SectionIndexer method getPositionForSection to get the item position inside the listView. We can then override the onTouch method in our custom component like that:
[java]@Overridepublic boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
if (x < sx)
return super.onTouchEvent(event);
else {
// We touched the index bar
float y = event.getY() – this.getPaddingTop() – getPaddingBottom();
int currentPosition = (int) Math.floor(y / indexSize);
section = sections[currentPosition];
this.setSelection(((SectionIndexer) getAdapter())
.getPositionForSection(currentPosition));
}
break;
}
case MotionEvent.ACTION_MOVE: {
if (x < sx)
return super.onTouchEvent(event);
else {
float y = event.getY();
int currentPosition = (int) Math.floor(y / indexSize);
section = sections[currentPosition];
this.setSelection(((SectionIndexer) getAdapter())
.getPositionForSection(currentPosition));
}
break;
}
}
return super.onTouchEvent(event);
}[/java]
Beatify the Listview
Once we made Android Listview sectionindexer and make it working as expected, we can make some improvements to our custom list view. For example, one simple thing we can do is showing the selected letter in the middle of the screen. This can be done quite easily modifying the onDraw method so that it shows the selected index letter. We can add this piece of code inside the onDraw method:
[java]// We draw the letter in the middleif (showLetter && section != null && !section.equals(“”)) {
Paint textPaint2 = new Paint();
textPaint2.setColor(Color.DKGRAY);
textPaint2.setTextSize(2 * indWidth);
canvas.drawText(section.toUpperCase(),
getWidth() / 2, getHeight() / 2,
textPaint2);
}
[/java]
Now when the user moves up his finger, we have to hide after some time this letter. It can be done in two steps: first, we intercept the MotionEvent.ACTION_UP event and then we send a message to a handler that removes this letter. So the onTouchEvent method becomes:
[java]listHandler = new ListHandler();listHandler.sendEmptyMessageDelayed(0, 30 * 1000);[/java]
and the ListHandler is shown below:
[java]private class ListHandler extends Handler {@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
showLetter = false;
FastSearchListView.this.invalidate();
}
}[/java]
Summary
At the end of this post, you gained the knowledge how to use Android ListView with SectionIndex.
Getting message – This page is taking too long to load
I'm sorry if it takes too muc time to load.
Thanks it works now. Now, the problem is that I have implemented this list view in an android dialog and on clicking the list item it doesn't close the dialog. It is not even calling the onclick event of list view. Not sure if it is clashing with onTouchEvent?
I'm interested on your comment because i didn't consider this aspect, using the listview in a dialog. I will test it. If you can you can send to me the skeleton you are using so that i can do it faster.
so basically I have created a custom spinner which when you click on opens the dialog with list view on it. Now when you select item it should close the dialog and selects the spinner with the selected option. If you comment the onTouchEvent() method, it works fine but I want to implement both of the functionalities in my custom spinner.
I have overridden performClick method of spinner
@Override
public boolean performClick(){
Context context = getContext();
SpinnerDialog lDialog = new SpinnerDialog(context, getSelectedItemPosition(), (ListAdapter) getAdapter(), this);
lDialog.show();
lDialog.getWindow().setLayout(400, 600);
return true;
}
This is the spinner dialog class
public class SpinnerDialog extends Dialog implements AdapterView.OnItemClickListener {
DialogInterface.OnClickListener mListener;
DigListView listView;
SpinnerDialog(Context inContex, int inPosition,
ListAdapter inSpinnerAdapter,
DialogInterface.OnClickListener inListener) {
super(inContex);
this.setContentView(R.layout.activity_listview);
this.setTitle("Select Item"); // Can get from the displayname property
mListener = inListener;
listView = (DigListView) findViewById(R.id.listview);
listView.setAdapter(inSpinnerAdapter);
listView.setOnItemClickListener(this);
listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
listView.setSelection(inPosition);
}
@Override
public void onItemClick(AdapterView inParent, View inView, int inPosition, long inId) {
if(mListener != null) {
mListener.onClick(this, inPosition);
}
}
}
The spinner implements DialogInterface.OnClickListener and so I have overridden onClick event
@Override
public void onClick(DialogInterface dialog, int which) {
setSelection(which);
dialog.dismiss();
}
Thanks for the example. One bug I came across was currentPosition, in FastSearchListView, would go out of bounds if I tapped or dragged off the top or bottom of the screen. I modified the code to bound currentPosition to [0..sections.length] like so:
int currentPosition = (int) Math.floor(y / indexSize);
currentPosition = Math.max( 0, currentPosition );
currentPosition = Math.min( sections.length, currentPosition );
Thx for your suggestion. I will try to reproduce the error and then i will modify the code. Thx for your support again.
Sorry our author, I have a wondering here. When I stroke to the customized listview (Up or Down) it scrolls a small distance. That's not as smooth as the original listview. So, what do I have to do to make it smooth like the original?
please upload code for indexbar dynamic set fullscreen all A to Z allphabets in screen ?
Something weird is happening with the timeout of the letter that is painted on the screen.
It's about this code:
listHandler.sendEmptyMessageDelayed(0, 30 * 1000);
private class ListHandler extends Handler {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
showLetter = false;
FastSearchListView.this.invalidate();
}
} – See more at: https://www.survivingwithandroid.com/2012/12/android-listview-sectionindexer-fastscroll.html#sthash.EVMe4B6k.dpuf
private class ListHandler extends Handler {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
showLetter = false;
FastSearchListView.this.invalidate();
}
} – See more at: https://www.survivingwithandroid.com/2012/12/android-listview-sectionindexer-fastscroll.html#sthash.EVMe4B6k.dpuf
private class ListHandler extends Handler {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
showLetter = false;
FastSearchListView.this.invalidate();
}
} – See more at: https://www.survivingwithandroid.com/2012/12/android-listview-sectionindexer-fastscroll.html#sthash.EVMe4B6k.dpuf
If I understand correctly this sets a 30 seconds delay before the letter is removed from the screen again. (this is very long, but ok)
However, after this delay, once the letter has been removed, it doesn't show again when I use the scrollbar again.
Oops, posted the code multiple times by accident.
Oops, posted the code multiple times by accident.
If you make currentPostion to a classmember instead, you can then in the onDraw() method do this:
for(int i = 0; i < sections.length; i++) {
if(i == currentPosition) {
….
}
}
to make something to mark the chosen letter, and then make a custom OnScrollListener, and put this is onScroll():
if(sections != null && isFastScrollEnabled()) {
RelativeLayout childView = (RelativeLayout) getChildAt(0);
String letter = ((TextView)childView.findViewById(R.id.tv_title)).getText().toString().substring(0, 1);
String[] sections = (String[]) ((SectionIndexer) getAdapter()).getSections();
for(int i = 0; i < sections.length; i++) {
if(letter.equals(sections[i])) {
currentPosition = i;
break;
}
}
}
It will make it more interactive and responsive
Also, @gnichola's suggestion is a necessary one
If you make currentPostion to a classmember instead, you can then in the onDraw() method do this:
for(int i = 0; i < sections.length; i++) {
if(i == currentPosition) {
….
}
}
to make something to mark the chosen letter, and then make a custom OnScrollListener, and put this is onScroll():
if(sections != null && isFastScrollEnabled()) {
RelativeLayout childView = (RelativeLayout) getChildAt(0);
String letter = ((TextView)childView.findViewById(R.id.tv_title)).getText().toString().substring(0, 1);
String[] sections = (String[]) ((SectionIndexer) getAdapter()).getSections();
for(int i = 0; i < sections.length; i++) {
if(letter.equals(sections[i])) {
currentPosition = i;
break;
}
}
}
I will make it more interactive and responsive
Sorry our author, I have a wondering here. When I stroke to the customized listview (Up or Down) it scrolls a small distance. That’s not as smooth as the original listview. So, what do I have to do to make it smooth like the original?
Sorry our author, I have a wondering here. When I stroke to the customized listview (Up or Down) it scrolls a small distance. That's not as smooth as the original listview. So, what do I have to do to make it smooth like the original?
Though it looks nice, I don’t think this method can support all cases. For example, what if there are more than the English alphabet to show?
It will get very condensed and un-readable. I prefer the way that default fast-scroller is shown.
I’ve also made a POC to show how to do it like the Lollipop’s contacts app, here:
https://github.com/AndroidDeveloperLB/LollipopContactsRecyclerViewFastScroller
What I’d like to know is how to do the same for ListView, and also maybe customize it to look like the way it looks on Lollipop (including the bubble).
Though it looks nice, I don't think this method can support all cases. For example, what if there are more than the English alphabet to show?
It will get very condensed and un-readable. I prefer the way that default fast-scroller is shown.
I've also made a POC to show how to do it like the Lollipop's contacts app, here:
https://github.com/AndroidDeveloperLB/LollipopContactsRecyclerViewFastScroller
What I'd like to know is how to do the same for ListView, and also maybe customize it to look like the way it looks on Lollipop (including the bubble).
I did implemented this. Now I cant set the height of list view. It takes fullscreen view. It covers a button below it. Is there any way to set margin for this lisview? Thanks
Yes of course, it depends on the way you created the layout. Probabily you set layout_height = match_parent.
If you like you can post the layout.
I have even tried with setting height as 100sp.