Main menu

Advanced Android AdapterView / CursorAdapter coding - Part 1

Some info: although this tutorial shows how to create a very specific and probably useless (outside od XenoAmp) AdapterView, it describes advanced concepts that will allow you to create any kind of adapter you can imagine. It seems this is not a common and easily available knowledge, judging from the amount of people looking for readily available code of i.e. HorizontalListView. So - after reading this you will be able to

1) create Adapter that presents data in multidimensional way, instead of standard, linear way
2) create CustomAdapterView that presents your multidimensional data any way you like it, even totally stupid and unordered

It took about 10 iterations to get XenoAmp music view to the place it is now: almost standard CursorAdapter used in almost standard AdapterView. Although first versions of music view were faster, fancier and had more of a WOW! effect due to possibility of zooming from your whole music collection to single tracks, it would ultimately fail with big music collections and would be a nightmare to maintain, as it was just a single View derivative that did all the fancy things.

And yes - at some point it began causing problems for users: it was working too slow, it couldn't properly support quick access index, it was a resource hog. And honestly a piece of crappy code. But if you don't know XenoAmp you have no idea what I am talking aobut, right?

 

XenoAmp music view is a Fragment that shows your music collection in multiple rows, each row can have variable number of items. You can scroll the view up and down, plus you can grab each row left or right. It's like a 2D matrix with variable row length, getting data straight from a view in SQLite database, you can decide what criteria you use for X and Y axii (here you can see publishing date vs album name). You don't have such component in Android! All of existing Adapters are linear! So first attempt at fixing it was creating ArrayAdapter for rows that created HorizontalListView plus ArrayAdapters for items in each row... It was even worse than before in every way imaginable, as you may suspect:

RowAdapter [
  HorizontalListView(RowItemAdapter1 [ Item1, Item2, Item3 ])
  HorizontalListView(RowItemAdapter2 [ Item1 ])
  HorizontalListView(RowItemAdapter3 [ Item1 Item2 ])
  HorizontalListView(RowItemAdapter4 [ Item1 Item2 Item3... ])
  ...
  HorizontalListView(RowItemAdapterN [ ... ])
]

You get it? Scrolling up and down would create a lot of new HorizontalListViews with proper Adapters... OK, I'm too ashamed to talk about it, so let's stop here and see the right way to do it.

Since all Android Adapters are linear (have only one dimension) we have to start with creating our own abstractions for 2D Adapter. Because we can create adapters based on different data stuctures like Cursor or Vector of LinkedLists and because it will be used later by fast scroller, we need this interface first:

public interface MatrixAdapterInterface extends Adapter {
    int getRowCount();
    int getColCount(int row);
    Object getItem(int row, int col);
    View getView(int row, int col, View convertView, ViewGroup parent);
    boolean isEmpty();
}

It's not a real mathematical matrix, as each row can have a variable number of columns (that's why there's a row argument to getColCount method), but still why don't we call it MatrixAdapter, eh? Besides if you need a proper matrix you can easily modify the above, right? convertView in getView method  is a standard Android way of recycling used Views, that means - using pre-inflated Views instead of inflating it every time from xml resource (the Google guys claim it makes things faster and less memory hungry, and they tend to know what they say, so... let's just do what they say...).

Now, since most of the time we would like to use our 2D adapter the same way as a standard Android AdapterView and (even better reason!) because we want to use all goodies that GroupView provides, we have to create generic MatrixAdapterView by deriving from AdapterView. We allow the class to accept only MatrixAdapter, not standard Android adapter, though:

public abstract class MatrixAdapterView extends AdapterView {

    OnSheetItemClickListener mOnItemClickListener;
    OnSheetItemLongClickListener mOnItemLongClickListener;
    
    public MatrixAdapterView(Context context) {
        super(context);
    }

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

    public MatrixAdapterView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

    }

    public interface OnSheetItemClickListener {
        void onItemClick(AdapterView<?> parent, View view, int row, int col, long id);
    }

    public void setOnItemClickListener(OnSheetItemClickListener listener) {
        mOnItemClickListener = listener;
    }

    public final OnSheetItemClickListener getOnRCItemClickListener() {
        return mOnItemClickListener;
    }

    public boolean performItemClick(View view, int row, int col, long id) {
        if (mOnItemClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            mOnItemClickListener.onItemClick(this, view, row, col, id);
            return true;
        }

        return false;
    }

    public interface OnSheetItemLongClickListener {
        boolean onItemLongClick(AdapterView<?> parent, View view, int row, int col, long id);
    }

    public void setOnItemLongClickListener(OnSheetItemLongClickListener listener) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        mOnItemLongClickListener = listener;
    }

    public final OnSheetItemLongClickListener getOnRCItemLongClickListener() {
        return mOnItemLongClickListener;
    }

    public int getRowCount() {
    	return ((MatrixAdapterInterface) getAdapter()).getRowCount();
    }
    
    public int getColCount(int row) {
    	return ((MatrixAdapterInterface) getAdapter()).getColCount(row);
    }
}

This will allow you to set a click and long click listeners for each "cell" in the "matrix" (versus position for linear data) and emit a standard click sound, for each class that you derive from MatrixAdapterView. May look complicated, but that's basicly all this class does.

And that was the easy part. To make it really work you need two more components:

1) Actual adapter (let's assume it's called ChessBoardCursorAdapter) that will bind and map your data to rows and columns, it will implement MatrixAdapterInterface and maybe SectionIndexer and return Views for each row/column.

2) A kind of layout class that will derive from MatrixAdapterView<ChessBoardCursorAdapter> and place the generated Views at proper x,y screen coordinates based on row,column and all scrollers that you would like to implement (for XenoAmp it is horizontal scroller for everything and vertical scrollers for each row).

3) Implement your own FastScroller (if needed), as the default... is not visible for you to use (sheeeesh... what's wrong with you, Google guys?!)

Two first topics will be described in next parts.

FacebookG+TwitterRSS