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()]( https://developer.android.com/reference/android/widget/ListView.html#setSelectionFromTop(int, int)) 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!