ListView - Keeping Position

Last week Joaquim Verges asked me a question about ListView. I’ve finally got around to writing up this solution and post.

The Problem

To paraphrase Joaquim, the problem is:

I have a ListView populated from a CursorLoader. When I swap to a cursor with new items on top, I use setSelectionFromTop() to keep the ListView at the same visible position. Unfortunately the ListView changes position and flicks in between the calls.

The Solution

The solution is to selectively block ListView laying out it’s children. The flicker mentioned above is ListView laying out it’s children to display the new items at the old position. If we stop the children layout pass, we stop the visible change in position.

public class BlockingListView extends ListView {
 
    private boolean mBlockLayoutChildren;
 
    public BlockingListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
 
    public void setBlockLayoutChildren(boolean block) {
        mBlockLayoutChildren = block;
    }
 
    @Override
    protected void layoutChildren() {
        if (!mBlockLayoutChildren) {
            super.layoutChildren();
        }
    }
}

You can then use it as below:

int firstVisPos = mListView.getFirstVisiblePosition();
View firstVisView = mListView.getChildAt(0);
int top = firstVisView != null ? firstVisView.getTop() : 0;

// Block children layout for now 
mListView.setBlockLayoutChildren(true);

// Number of items added before the first visible item 
int itemsAddedBeforeFirstVisible = ...;

// Change the cursor, or call notifyDataSetChanged() if not using a Cursor
mAdapter.swapCursor(...);

// Let ListView start laying out children again 
mListView.setBlockLayoutChildren(false);

// Call setSelectionFromTop to change the ListView position
mListView.setSelectionFromTop(firstVisPos + itemsAddedBeforeFirstVisible, top);

And that’s it!