Brightec develop Android ListView with an alphabet scroller
Written by Chris Leversuch
Dec 10, 2013

Android ListView with an alphabet scroller

When presenting data in a ListView it may be helpful to have a scroller down the side to allow users to quickly jump to a specific letter.

Presenting data in a ListView

Before working on our activity, we’ll get the other components in place, starting with layouts. First, create a layout called list_alphabet and add the following XML:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >

    <ListView
        android:id="@android:id/list"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:fastScrollEnabled="true" />

    <LinearLayout
        android:id="@+id/sideIndex"
        android:layout_width="40dip"
        android:layout_height="fill_parent"
        android:background="#FFF"
        android:gravity="center_horizontal"
        android:orientation="vertical" >
    </LinearLayout>

</LinearLayout>

This gives us a ListView that fills most of the screen, apart from a 40dp column down the right hand side that we’ll put our alphabet scroller in. Next, create a layout called row_item with the following XML:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:padding="10dp" >

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView" />

</LinearLayout>

This will be the layout for each of our ListView rows. Finally, create a layout called row_section with the following XML:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/darker_gray"
    android:orientation="horizontal"
    android:paddingBottom="2dp"
    android:paddingLeft="10dp"
    android:paddingTop="2dp" >

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView"
        android:textColor="@android:color/white"
        android:textStyle="bold" />

</LinearLayout>

This will be the layout for the section headers in our ListView. Next we need to create an adapter that will provide the data to the ListView. Create a new class called AlphabetListAdapter that extends BaseAdapter. The first thing we’ll do in this class is create some models to define our list data:

public static abstract class Row {}

public static final class Section extends Row {
    public final String text;

    public Section(String text) {
        this.text = text;
    }
}

public static final class Item extends Row {
    public final String text;

    public Item(String text) {
        this.text = text;
    }
}

The first is a base class that all the list data will extend, then there is one for a section row and another for an item row. Next we define a property to hold our rows and a setter method:

private List rows;

public void setRows(List rows) {
    this.rows = rows;
}

When you created your class, you should have found some stub methods that need changing so that they use the property that we’ve just defined. These should be:

@Override
public int getCount() {
    return rows.size();
}

@Override
public Row getItem(int position) {
    return rows.get(position);
}

@Override
public long getItemId(int position) {
    return position;
}

The getView method provides a convertView property. This is basically to reduce memory usage - as a row scrolls off the top of the screen, Android will give that View object back to the adapter for re-use so that you don’t have to create more objects than are currently visible. For this to work properly, you need to tell Android how many view types you’ve got and which view type each row is. This is so that you are only given a suitable View object, otherwise you might be given a View object that doesn’t have the right subviews. Since we have 2 view types - item and section - we need to add a couple of methods to the adapter:

@Override
public int getViewTypeCount() {
    return 2;
}

@Override
public int getItemViewType(int position) {
    if (getItem(position) instanceof Section) {
        return 1;
    } else {
        return 0;
    }
}

We can now define our getView method:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View view = convertView;
    
    if (getItemViewType(position) == 0) { // Item
        if (view == null) {
            LayoutInflater inflater = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            view = (LinearLayout) inflater.inflate(R.layout.row_item, parent, false);  
        }
        
        Item item = (Item) getItem(position);
        TextView textView = (TextView) view.findViewById(R.id.textView1);
        textView.setText(item.text);
    } else { // Section
        if (view == null) {
            LayoutInflater inflater = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            view = (LinearLayout) inflater.inflate(R.layout.row_section, parent, false);  
        }
        
        Section section = (Section) getItem(position);
        TextView textView = (TextView) view.findViewById(R.id.textView1);
        textView.setText(section.text);
    }
    
    return view;
}

This inflates the relevant view and populates the TextView. With these in place, we can now create our Activity. Create a new class that extends ListActivity and define the following properties:

private AlphabetListAdapter adapter = new AlphabetListAdapter();
private GestureDetector mGestureDetector;
private List<Object[]> alphabet = new ArrayList<Object[]>();
private HashMap<String, Integer> sections = new HashMap<String, Integer>();
private int sideIndexHeight;
private static float sideIndexX;
private static float sideIndexY;
private int indexListSize;

We also need a custom GestureDetector:

class SideIndexGestureListener extends GestureDetector.SimpleOnGestureListener {
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        // we know already coordinates of first touch
        // we know as well a scroll distance
        sideIndexX = sideIndexX - distanceX;
        sideIndexY = sideIndexY - distanceY;

        // when the user scrolls within our side index
        // we can show for every position in it a proper
        // item in the country list
        if (sideIndexX >= 0 && sideIndexY >= 0) {
            displayListItem();
        }

        return super.onScroll(e1, e2, distanceX, distanceY);
    }
}

Before writing the onCreate method we’ll define some other methods: populateCountries() just gives us some test data for the ListView:

private List populateCountries() {
    List countries = new ArrayList();
    countries.add("Afghanistan");
    countries.add("Albania");
    countries.add("Bahrain");
    countries.add("Bangladesh");
    countries.add("Cambodia");
    countries.add("Cameroon");
    countries.add("Denmark");
    countries.add("Djibouti");
    countries.add("East Timor");
    countries.add("Ecuador");
    countries.add("Fiji");
    countries.add("Finland");
    countries.add("Gabon");
    countries.add("Georgia");
    countries.add("Haiti");
    countries.add("Holy See");
    countries.add("Iceland");
    countries.add("India");
    countries.add("Jamaica");
    countries.add("Japan");
    countries.add("Kazakhstan");
    countries.add("Kenya");
    countries.add("Laos");
    countries.add("Latvia");
    countries.add("Macau");
    countries.add("Macedonia");
    countries.add("Namibia");
    countries.add("Nauru");
    countries.add("Oman");
    countries.add("Pakistan");
    countries.add("Palau");
    countries.add("Qatar");
    countries.add("Romania");
    countries.add("Russia");
    countries.add("Saint Kitts and Nevis");
    countries.add("Saint Lucia");
    countries.add("Taiwan");
    countries.add("Tajikistan");
    countries.add("Uganda");
    countries.add("Ukraine");
    countries.add("Vanuatu");
    countries.add("Venezuela");
    countries.add("Yemen");
    countries.add("Zambia");
    countries.add("Zimbabwe");
    countries.add("0");
    countries.add("2");
    countries.add("9");
    return countries;
}

displayListItem() is used by the GestureDetector to scroll to the relevant place in the ListView:

public void displayListItem() {
    LinearLayout sideIndex = (LinearLayout) findViewById(R.id.sideIndex);
    sideIndexHeight = sideIndex.getHeight();
    // compute number of pixels for every side index item
    double pixelPerIndexItem = (double) sideIndexHeight / indexListSize;

    // compute the item index for given event position belongs to
    int itemPosition = (int) (sideIndexY / pixelPerIndexItem);

    // get the item (we can do it since we know item index)
    if (itemPosition < alphabet.size()) {
        Object[] indexItem = alphabet.get(itemPosition);
        int subitemPosition = sections.get(indexItem[0]);

        //ListView listView = (ListView) findViewById(android.R.id.list);
        getListView().setSelection(subitemPosition);
    }
}

updateList() adds TextViews to the alphabet scroller:

public void updateList() {
    LinearLayout sideIndex = (LinearLayout) findViewById(R.id.sideIndex);
    sideIndex.removeAllViews();
    indexListSize = alphabet.size();
    if (indexListSize < 1) {
        return;
    }

    int indexMaxSize = (int) Math.floor(sideIndex.getHeight() / 20);
    int tmpIndexListSize = indexListSize;
    while (tmpIndexListSize > indexMaxSize) {
        tmpIndexListSize = tmpIndexListSize / 2;
    }
    double delta;
    if (tmpIndexListSize > 0) {
        delta = indexListSize / tmpIndexListSize;
    } else {
        delta = 1;
    }

    TextView tmpTV;
    for (double i = 1; i <= indexListSize; i = i + delta) {
        Object[] tmpIndexItem = alphabet.get((int) i - 1);
        String tmpLetter = tmpIndexItem[0].toString();

        tmpTV = new TextView(this);
        tmpTV.setText(tmpLetter);
        tmpTV.setGravity(Gravity.CENTER);
        tmpTV.setTextSize(15);
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 1);
        tmpTV.setLayoutParams(params);
        sideIndex.addView(tmpTV);
    }

    sideIndexHeight = sideIndex.getHeight();

    sideIndex.setOnTouchListener(new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            // now you know coordinates of touch
            sideIndexX = event.getX();
            sideIndexY = event.getY();

            // and can display a proper item it country list
            displayListItem();

            return false;
        }
    });
}

We can now define the onCreate method. After initialising our country list, we loop through it and populate the alphabet list, grouping numeric entries together. We then call updateList() to create the scroller views:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.list_alphabet);

    mGestureDetector = new GestureDetector(this, new SideIndexGestureListener());

    List countries = populateCountries();
    Collections.sort(countries);

    List rows = new ArrayList();
    int start = 0;
    int end = 0;
    String previousLetter = null;
    Object[] tmpIndexItem = null;
    Pattern numberPattern = Pattern.compile("[0-9]");

    for (String country : countries) {
        String firstLetter = country.substring(0, 1);

        // Group numbers together in the scroller
        if (numberPattern.matcher(firstLetter).matches()) {
            firstLetter = "#";
        }

        // If we've changed to a new letter, add the previous letter to the alphabet scroller
        if (previousLetter != null && !firstLetter.equals(previousLetter)) {
            end = rows.size() - 1;
            tmpIndexItem = new Object[3];
            tmpIndexItem[0] = previousLetter.toUpperCase(Locale.UK);
            tmpIndexItem[1] = start;
            tmpIndexItem[2] = end;
            alphabet.add(tmpIndexItem);

            start = end + 1;
        }

        // Check if we need to add a header row
        if (!firstLetter.equals(previousLetter)) {
            rows.add(new Section(firstLetter));
            sections.put(firstLetter, start);
        }

        // Add the country to the list
        rows.add(new Item(country));
        previousLetter = firstLetter;
    }

    if (previousLetter != null) {
        // Save the last letter
        tmpIndexItem = new Object[3];
        tmpIndexItem[0] = previousLetter.toUpperCase(Locale.UK);
        tmpIndexItem[1] = start;
        tmpIndexItem[2] = rows.size() - 1;
        alphabet.add(tmpIndexItem);
    }

    adapter.setRows(rows);
    setListAdapter(adapter);

    updateList();
}

Finally, define onTouchEvent() to call our custom GestureDetector:

@Override
public boolean onTouchEvent(MotionEvent event) {
    if (mGestureDetector.onTouchEvent(event)) {
        return true;
    } else {
        return false;
    }
}

Putting this all together should give you a working ListView with an alphabet scroller. Download the sample project

Top