Paul Brower bio photo

Paul Brower

Android development for fun and profit.

Email Twitter Github Stackoverflow Codementor

Find further code and implementation here: https://github.com/browep/AndroidAlphaIndexer

The iPhone has a nice widget for handling “jumps” in a list with alphabetical shortcuts on the right, see:

It’s very nice for getting through very large datasets that might otherwise take a very long time to scroll. This tutorial will have a complete example of how to implement one with source code.

It relies on the ListAdapter implementing an interface called the SectionIndexer. The section indexer allows the ListAdapter to communicate how many sections there are, what they’re labels are, what position corresponds to the correct section, and what section corresponds to what position.

Let’s create a simple layout and activity. This activity is going to have a ListView that has an Adapter that implements SectionIndexer, full file here

public class Main extends Activity {

  public static final String TAG = Main.class.getCanonicalName();

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    ListView listView = (ListView) findViewById(R.id.list_view);

    List<String> items = new ArrayList<String>();

    for (char ch : SideSelector.ALPHABET) {
      for (int i = 1; i <= 50; i++) {
        items.add(String.valueOf(ch) + "-" + i);
      }
    }

    listView.setAdapter(new IndexingArrayAdapter(this, android.R.layout.simple_list_item_1, items));

    SideSelector sideSelector = (SideSelector) findViewById(R.id.side_selector);
    sideSelector.setListView(listView);

  }

  public class IndexingArrayAdapter extends ArrayAdapter<String> implements SectionIndexer {
    public IndexingArrayAdapter(Context context, int textViewResourceId, List<String> objects) {
      super(context, textViewResourceId, objects);
    }

    public Object[] getSections() {
      String[] chars = new String[SideSelector.ALPHABET.length];
      for (int i = 0; i < SideSelector.ALPHABET.length; i++) {
        chars[i] = String.valueOf(SideSelector.ALPHABET[i]);
      }

      return chars;
    }

    public int getPositionForSection(int i) {

      Log.d(TAG, "getPositionForSection " + i);
      return (int) (getCount() * ((float)i/(float)getSections().length));
    }

    public int getSectionForPosition(int i) {
      return 0;
    }
  }

  }

and the layout, full file here

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  >

  <ListView
  android:id="@+id/list_view"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  />

  <com.github.browep.alphaindexer.SideSelector
  android:id="@+id/side_selector"
  android:layout_width="30dp"
  android:layout_height="match_parent"
  android:layout_alignParentRight="true"
  />
</RelativeLayout>

now we need a custom view.

public class SideSelector extends View {
    private static String TAG = SideSelector.class.getCanonicalName();

    public static char[] ALPHABET = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
    'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
    public static final int BOTTOM_PADDING = 10;

    private SectionIndexer selectionIndexer = null;
    private ListView list;
    private Paint paint;
    private String[] sections;

    public SideSelector(Context context) {
      super(context);
      init();
    }

    public SideSelector(Context context, AttributeSet attrs) {
      super(context, attrs);
      init();
    }

    public SideSelector(Context context, AttributeSet attrs, int defStyle) {
      super(context, attrs, defStyle);
      init();
    }

    private void init() {
      setBackgroundColor(0x44FFFFFF);
      paint = new Paint();
      paint.setColor(0xFFA6A9AA);
      paint.setTextSize(20);
      paint.setTextAlign(Paint.Align.CENTER);
    }

    public void setListView(ListView _list) {
      list = _list;
      selectionIndexer = (SectionIndexer) _list.getAdapter();

      Object[] sectionsArr = selectionIndexer.getSections();
      sections = new String[sectionsArr.length];
      for (int i = 0; i < sectionsArr.length; i++) {
        sections[i] = sectionsArr[i].toString();
      }

    }

    public boolean onTouchEvent(MotionEvent event) {
      super.onTouchEvent(event);
      int y = (int) event.getY();
      float selectedIndex = ((float) y / (float) getPaddedHeight()) * ALPHABET.length;

      if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE) {
        if (selectionIndexer == null) {
          selectionIndexer = (SectionIndexer) list.getAdapter();
        }
        int position = selectionIndexer.getPositionForSection((int) selectedIndex);
        if (position == -1) {
          return true;
        }
        list.setSelection(position);
      }
      return true;
    }

    protected void onDraw(Canvas canvas) {

      int viewHeight = getPaddedHeight();
      float charHeight = ((float) viewHeight) / (float) sections.length;

      float widthCenter = getMeasuredWidth() / 2;
      for (int i = 0; i < sections.length; i++) {
        canvas.drawText(String.valueOf(sections[i]), widthCenter, charHeight + (i * charHeight), paint);
      }
      super.onDraw(canvas);
    }

    private int getPaddedHeight() {
      return getHeight() - BOTTOM_PADDING;
    }
}

This view upon construction will initialize some styling variables. It will then need to be linked to the ListView through setListView. From the ListView it will get the adapter and then it’s sections. I have used the static list of the english alphabet for this example. When it is drawn in onDraw it will split the height of the view evenly according to how many sections it has and draw the string representation of each onto the view.

When it is touched or finger dragged it will determine which section was touched by dividing the y-touch coordinate by the height times the number of sections. We then ask the adapter what position this section corresponds to and tell the listview to scroll to it.