/* * Copyright (C) 2008 Josh Guilfoyle <jasta@devtcg.org> * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. */ package org.devtcg.five.widget; import java.util.HashMap; import android.content.Context; import android.database.DataSetObserver; import android.util.AttributeSet; import android.view.View; import android.widget.ListAdapter; import android.widget.ListView; /** * Special ListView class which can efficiently map adapter row ids to * list positions. This can be useful when UIs need to introduce stateful * UI changes not backed by the underlying adapter data set. */ public class StatefulListView extends ListView { /** Cache of searches to resolve row positions by adapter row id. */ protected HashMap<Long, Integer> mViewMapCache = null; public StatefulListView(Context context) { super(context); } public StatefulListView(Context context, AttributeSet attrs) { super(context, attrs); } @Override public void setAdapter(ListAdapter adapter) { super.setAdapter(adapter); adapter.registerDataSetObserver(mDataSetChanged); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); ListAdapter adapter = getAdapter(); if (adapter != null) adapter.unregisterDataSetObserver(mDataSetChanged); } private final DataSetObserver mDataSetChanged = new DataSetObserver() { @Override public void onChanged() { /* The cache must be cleared in the event that our ordering changed. * For example, if a new entry was inserted somewhere in the list * before our cached positions. */ if (mViewMapCache != null) mViewMapCache.clear(); } @Override public void onInvalidated() { onChanged(); } }; private int lazyListSearchForId(long id) { ListAdapter adapter = getAdapter(); int start = getFirstVisiblePosition(); int count = getChildCount(); assert adapter.getCount() > start + count; for (int i = start; i < start + count; i++) { if (adapter.getItemId(i) == id) return i; } return -1; } public int getRowFromId(long id) { if (mViewMapCache == null) mViewMapCache = new HashMap<Long, Integer>(); Integer row = mViewMapCache.get(id); /* Damn, have to search for the list position */ if (row == null) { if ((row = lazyListSearchForId(id)) == -1) return -1; mViewMapCache.put(id, row); } return row; } /** * Efficiently locate the row view by data set id. Only "visible" views * can be located this way, as the list view recycles views for other * off-screen items and thus those cannot be referenced directly. * * To complete this abstraction, you must also be careful to bind your * stateful data in your ListAdapter so that scrolling will not ignore * your UI changes. * * @param id * Item id as would be returned by {@link ListAdapter#getItemId(int)}. * @return * The child view if found in the list; null otherwise. */ public View getChildFromId(long id) { int row = getRowFromId(id); if (row < 0) return null; return getChildFromPos(row); } public View getChildFromPos(int pos) { int first = getFirstVisiblePosition(); /* View is no longer on screen... */ if (pos < first || pos > (first + getChildCount())) return null; return getChildAt(pos - first); } }