change game list item strategy since it turns out adapter's findView()

doesn't pass in the previous representation of a given item for
recycling: move async loading of summary into GameListItem class, and
use getChildAt() to invalidate a single list (rather than reloading
the whole list) whereever possible.  Still need to dump the list
whenever the number of items changes since we're depending on DBUtils
to determine the order and have no way to reshuffle existing items.
This commit is contained in:
Eric House 2012-12-10 07:48:15 -08:00
parent 1bc8070bb1
commit d820554ffb
5 changed files with 241 additions and 243 deletions

View file

@ -63,7 +63,7 @@ public class DBUtils {
private static long[] s_cachedRowIDs = null;
public static interface DBChangeListener {
public void gameSaved( long rowid );
public void gameSaved( long rowid, boolean countChanged );
}
private static HashSet<DBChangeListener> s_listeners =
new HashSet<DBChangeListener>();
@ -319,8 +319,8 @@ public class DBUtils {
clearRowIDsCache();
}
}
notifyListeners( rowid );
db.close();
notifyListeners( rowid, false );
}
} // saveSummary
@ -376,7 +376,7 @@ public class DBUtils {
public static void setMsgFlags( long rowid, int flags )
{
setInt( rowid, DBHelper.HASMSGS, flags );
notifyListeners( rowid );
notifyListeners( rowid, false );
}
public static void setExpanded( long rowid, boolean expanded )
@ -703,7 +703,7 @@ public class DBUtils {
clearRowIDsCache();
lock = new GameUtils.GameLock( rowid, true ).lock();
notifyListeners( rowid );
notifyListeners( rowid, true );
}
return lock;
@ -727,8 +727,8 @@ public class DBUtils {
updateRow( context, DBHelper.TABLE_NAME_SUM, rowid, values );
setCached( rowid, null ); // force reread
if ( -1 != rowid ) { // Is this possible? PENDING
notifyListeners( rowid );
if ( -1 != rowid ) { // Means new game?
notifyListeners( rowid, false );
}
return rowid;
}
@ -777,7 +777,7 @@ public class DBUtils {
db.close();
}
clearRowIDsCache();
notifyListeners( lock.getRowid() );
notifyListeners( lock.getRowid(), true );
}
public static long[] gamesList( Context context )
@ -1223,12 +1223,12 @@ public class DBUtils {
}
}
private static void notifyListeners( long rowid )
private static void notifyListeners( long rowid, boolean countChanged )
{
synchronized( s_listeners ) {
Iterator<DBChangeListener> iter = s_listeners.iterator();
while ( iter.hasNext() ) {
iter.next().gameSaved( rowid );
iter.next().gameSaved( rowid, countChanged );
}
}
}

View file

@ -21,7 +21,6 @@ package org.eehouse.android.xw4;
import android.content.Context;
import android.database.DataSetObserver;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.view.LayoutInflater;
@ -31,11 +30,11 @@ import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import java.io.FileInputStream;
import java.text.DateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Random;
import junit.framework.Assert;
@ -46,84 +45,29 @@ import org.eehouse.android.xw4.jni.CurGameInfo.DeviceRole;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
public class GameListAdapter extends XWListAdapter {
private static final boolean s_isFire;
private static Random s_random;
static {
s_isFire = Build.MANUFACTURER.equals( "Amazon" );
if ( s_isFire ) {
s_random = new Random();
}
}
private Context m_context;
private ListView m_list;
private LayoutInflater m_factory;
private int m_fieldID;
private Handler m_handler;
private DateFormat m_df;
private LoadItemCB m_cb;
// Track those rows known to be good. If a rowid is not in this
// set, assume it must be loaded. Add rowids to this set as
// they're loaded, and remove one when when it must be redrawn.
private HashSet<Long> m_loadedRows;
public interface LoadItemCB {
public void itemClicked( long rowid, GameSummary summary );
}
private class LoadItemTask extends AsyncTask<Void, Void, GameSummary> {
private GameListItem m_view;
private Context m_context;
// private int m_id;
public LoadItemTask( Context context, GameListItem view )
{
DbgUtils.logf( "Creating LoadItemTask for row %d",
view.getRowID() );
m_context = context;
m_view = view;
}
@Override
protected GameSummary doInBackground( Void... unused )
{
// Without this, on the Fire only the last item in the
// list it tappable. Likely my fault, but this seems to
// work around it.
if ( s_isFire ) {
try {
int sleepTime = 500 + (s_random.nextInt() % 500);
Thread.sleep( sleepTime );
} catch ( Exception e ) {
}
}
long rowid = m_view.getRowID();
GameSummary summary = DBUtils.getSummary( m_context, rowid, 1500 );
return summary;
} // doInBackground
@Override
protected void onPostExecute( GameSummary summary )
{
setData( m_view, summary );
setLoaded( m_view.getRowID() );
m_view.setLoaded( true );
DbgUtils.logf( "LoadItemTask for row %d finished",
m_view.getRowID() );
}
} // class LoadItemTask
public GameListAdapter( Context context, Handler handler, LoadItemCB cb,
String fieldName ) {
public GameListAdapter( Context context, ListView list,
Handler handler, LoadItemCB cb, String fieldName ) {
super( DBUtils.gamesList(context).length );
m_context = context;
m_list = list;
m_handler = handler;
m_cb = cb;
m_factory = LayoutInflater.from( context );
m_df = DateFormat.getDateTimeInstance( DateFormat.SHORT,
DateFormat.SHORT );
m_loadedRows = new HashSet<Long>();
m_fieldID = fieldToID( fieldName );
}
@ -136,53 +80,29 @@ public class GameListAdapter extends XWListAdapter {
// When one needs loading it's done via an async task.
public View getView( int position, View convertView, ViewGroup parent )
{
GameListItem result;
boolean mustLoad = false;
if ( null == convertView ) {
result = (GameListItem)
m_factory.inflate( R.layout.game_list_item, null );
result.setRowID( DBUtils.gamesList(m_context)[position] );
mustLoad = true;
} else {
result = (GameListItem)convertView;
long rowid = result.getRowID();
if ( isDirty(rowid) || !result.isLoaded() ) {
mustLoad = true;
}
}
if ( mustLoad ) {
new LoadItemTask( m_context, result ).execute();
}
GameListItem result = (GameListItem)
m_factory.inflate( R.layout.game_list_item, null );
result.init( m_handler, DBUtils.gamesList(m_context)[position],
m_fieldID, m_cb );
return result;
}
public void inval( long rowid )
{
synchronized( m_loadedRows ) {
m_loadedRows.remove( rowid );
GameListItem child = getItemFor( rowid );
if ( null != child ) {
child.forceReload();
} else {
DbgUtils.logf( "no child for rowid %d", rowid );
m_list.invalidate();
}
}
private void dirtyAll()
public void invalName( long rowid )
{
synchronized( m_loadedRows ) {
m_loadedRows.clear();
}
}
private boolean isDirty( long rowid )
{
synchronized( m_loadedRows ) {
return ! m_loadedRows.contains( rowid );
}
}
private void setLoaded( long rowid )
{
synchronized( m_loadedRows ) {
m_loadedRows.add( rowid );
GameListItem item = getItemFor( rowid );
if ( null != item ) {
item.invalName();
}
}
@ -201,12 +121,24 @@ public class GameListAdapter extends XWListAdapter {
+ " from %d to %d", m_fieldID, newID );
}
m_fieldID = newID;
dirtyAll();
// return true so caller will do onContentChanged.
// There's no other way to signal GameListItem instances
// since we don't maintain a list of them.
changed = true;
}
return changed;
}
private GameListItem getItemFor( long rowid )
{
GameListItem result = null;
int position = positionFor( rowid );
if ( 0 <= position ) {
result = (GameListItem)m_list.getChildAt( position );
}
return result;
}
private int fieldToID( String fieldName )
{
int[] ids = {
@ -225,106 +157,16 @@ public class GameListAdapter extends XWListAdapter {
return result;
}
private void setData( GameListItem layout, final GameSummary summary )
private int positionFor( long rowid )
{
if ( null != summary ) {
final long rowid = layout.getRowID();
String state = summary.summarizeState();
TextView view = (TextView)layout.findViewById( R.id.game_name );
String value = null;
switch ( m_fieldID ) {
case R.string.game_summary_field_empty:
break;
case R.string.game_summary_field_language:
value =
DictLangCache.getLangName( m_context,
summary.dictLang );
break;
case R.string.game_summary_field_opponents:
value = summary.playerNames();
break;
case R.string.game_summary_field_state:
value = state;
int position = -1;
long[] rowids = DBUtils.gamesList( m_context );
for ( int ii = 0; ii < rowids.length; ++ii ) {
if ( rowids[ii] == rowid ) {
position = ii;
break;
}
String name = GameUtils.getName( m_context, rowid );
if ( null != value ) {
value = m_context.getString( R.string.str_game_namef,
name, value );
} else {
value = name;
}
view.setText( value );
layout.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick( View v ) {
m_cb.itemClicked( rowid, summary );
}
} );
LinearLayout list =
(LinearLayout)layout.findViewById( R.id.player_list );
boolean haveATurn = false;
boolean haveALocalTurn = false;
boolean[] isLocal = new boolean[1];
for ( int ii = 0; ii < summary.nPlayers; ++ii ) {
ExpiringLinearLayout tmp = (ExpiringLinearLayout)
m_factory.inflate( R.layout.player_list_elem, null );
view = (TextView)tmp.findViewById( R.id.item_name );
view.setText( summary.summarizePlayer( ii ) );
view = (TextView)tmp.findViewById( R.id.item_score );
view.setText( String.format( " %d", summary.scores[ii] ) );
boolean thisHasTurn = summary.isNextToPlay( ii, isLocal );
if ( thisHasTurn ) {
haveATurn = true;
if ( isLocal[0] ) {
haveALocalTurn = true;
}
}
tmp.setPct( m_handler, thisHasTurn, isLocal[0],
summary.lastMoveTime );
list.addView( tmp, ii );
}
view = (TextView)layout.findViewById( R.id.state );
view.setText( state );
view = (TextView)layout.findViewById( R.id.modtime );
long lastMoveTime = summary.lastMoveTime;
lastMoveTime *= 1000;
view.setText( m_df.format( new Date( lastMoveTime ) ) );
int iconID;
ImageView marker =
(ImageView)layout.findViewById( R.id.msg_marker );
CommsConnType conType = summary.conType;
if ( CommsConnType.COMMS_CONN_RELAY == conType ) {
iconID = R.drawable.relaygame;
} else if ( CommsConnType.COMMS_CONN_BT == conType ) {
iconID = android.R.drawable.stat_sys_data_bluetooth;
} else if ( CommsConnType.COMMS_CONN_SMS == conType ) {
iconID = android.R.drawable.sym_action_chat;
} else {
iconID = R.drawable.sologame;
}
marker.setImageResource( iconID );
view = (TextView)layout.findViewById( R.id.role );
String roleSummary = summary.summarizeRole();
if ( null != roleSummary ) {
view.setText( roleSummary );
} else {
view.setVisibility( View.GONE );
}
boolean expanded = DBUtils.getExpanded( m_context, rowid );
layout.update( m_handler, expanded, summary.lastMoveTime,
haveATurn, haveALocalTurn );
}
return position;
}
}

View file

@ -21,11 +21,19 @@
package org.eehouse.android.xw4;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.text.DateFormat;
import java.util.Date;
import org.eehouse.android.xw4.jni.GameSummary;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
public class GameListItem extends LinearLayout
implements View.OnClickListener {
@ -39,6 +47,9 @@ public class GameListItem extends LinearLayout
private long m_lastMoveTime;
private ImageButton m_expandButton;
private Handler m_handler;
private GameSummary m_summary;
private GameListAdapter.LoadItemCB m_cb;
private int m_fieldID;
public GameListItem( Context cx, AttributeSet as )
{
@ -49,11 +60,31 @@ public class GameListItem extends LinearLayout
m_lastMoveTime = 0;
}
public void update( Handler handler, boolean expanded,
long lastMoveTime, boolean haveTurn,
boolean haveTurnLocal )
public void init( Handler handler, long rowid, int fieldID,
GameListAdapter.LoadItemCB cb )
{
m_handler = handler;
m_rowid = rowid;
m_fieldID = fieldID;
m_cb = cb;
forceReload();
}
public void forceReload()
{
m_summary = null;
new LoadItemTask().execute();
}
public void invalName()
{
setName();
}
private void update( boolean expanded, long lastMoveTime, boolean haveTurn,
boolean haveTurnLocal )
{
m_expanded = expanded;
m_lastMoveTime = lastMoveTime;
m_haveTurn = haveTurn;
@ -65,28 +96,6 @@ public class GameListItem extends LinearLayout
showHide();
}
public void setLoaded( boolean loaded )
{
if ( m_loaded != loaded ) {
m_loaded = loaded;
// This should be enough to invalidate
findViewById( R.id.view_unloaded )
.setVisibility( loaded ? View.GONE : View.VISIBLE );
findViewById( R.id.view_loaded )
.setVisibility( loaded ? View.VISIBLE : View.GONE );
}
}
public boolean isLoaded()
{
return m_loaded;
}
public void setRowID( long rowid )
{
m_rowid = rowid;
}
public long getRowID()
{
return m_rowid;
@ -99,6 +108,18 @@ public class GameListItem extends LinearLayout
showHide();
}
private void setLoaded()
{
if ( !m_loaded ) {
m_loaded = true;
// This should be enough to invalidate
findViewById( R.id.view_unloaded )
.setVisibility( m_loaded ? View.GONE : View.VISIBLE );
findViewById( R.id.view_loaded )
.setVisibility( m_loaded ? View.VISIBLE : View.GONE );
}
}
private void showHide()
{
m_expandButton.setImageResource( m_expanded ?
@ -111,5 +132,137 @@ public class GameListItem extends LinearLayout
m_haveTurnLocal, m_lastMoveTime );
}
private String setName()
{
String state = null; // hack to avoid calling summarizeState twice
if ( null != m_summary ) {
state = m_summary.summarizeState();
TextView view = (TextView)findViewById( R.id.game_name );
String value = null;
switch ( m_fieldID ) {
case R.string.game_summary_field_empty:
break;
case R.string.game_summary_field_language:
value =
DictLangCache.getLangName( m_context,
m_summary.dictLang );
break;
case R.string.game_summary_field_opponents:
value = m_summary.playerNames();
break;
case R.string.game_summary_field_state:
value = state;
break;
}
if ( null != value ) {
String name = GameUtils.getName( m_context, m_rowid );
value = m_context.getString( R.string.str_game_namef,
name, value );
} else {
value = GameUtils.getName( m_context, m_rowid );
}
view.setText( value );
}
return state;
}
private void setData()
{
if ( null != m_summary ) {
TextView view;
String state = setName();
setOnClickListener( new View.OnClickListener() {
@Override
public void onClick( View v ) {
m_cb.itemClicked( m_rowid, m_summary );
}
} );
LinearLayout list =
(LinearLayout)findViewById( R.id.player_list );
list.removeAllViews();
boolean haveATurn = false;
boolean haveALocalTurn = false;
boolean[] isLocal = new boolean[1];
for ( int ii = 0; ii < m_summary.nPlayers; ++ii ) {
ExpiringLinearLayout tmp = (ExpiringLinearLayout)
Utils.inflate( m_context, R.layout.player_list_elem );
view = (TextView)tmp.findViewById( R.id.item_name );
view.setText( m_summary.summarizePlayer( ii ) );
view = (TextView)tmp.findViewById( R.id.item_score );
view.setText( String.format( " %d", m_summary.scores[ii] ) );
boolean thisHasTurn = m_summary.isNextToPlay( ii, isLocal );
if ( thisHasTurn ) {
haveATurn = true;
if ( isLocal[0] ) {
haveALocalTurn = true;
}
}
tmp.setPct( m_handler, thisHasTurn, isLocal[0],
m_summary.lastMoveTime );
list.addView( tmp, ii );
}
view = (TextView)findViewById( R.id.state );
view.setText( state );
view = (TextView)findViewById( R.id.modtime );
long lastMoveTime = m_summary.lastMoveTime;
lastMoveTime *= 1000;
DateFormat df = DateFormat.getDateTimeInstance( DateFormat.SHORT,
DateFormat.SHORT );
view.setText( df.format( new Date( lastMoveTime ) ) );
int iconID;
ImageView marker =
(ImageView)findViewById( R.id.msg_marker );
CommsConnType conType = m_summary.conType;
if ( CommsConnType.COMMS_CONN_RELAY == conType ) {
iconID = R.drawable.relaygame;
} else if ( CommsConnType.COMMS_CONN_BT == conType ) {
iconID = android.R.drawable.stat_sys_data_bluetooth;
} else if ( CommsConnType.COMMS_CONN_SMS == conType ) {
iconID = android.R.drawable.sym_action_chat;
} else {
iconID = R.drawable.sologame;
}
marker.setImageResource( iconID );
view = (TextView)findViewById( R.id.role );
String roleSummary = m_summary.summarizeRole();
if ( null != roleSummary ) {
view.setText( roleSummary );
} else {
view.setVisibility( View.GONE );
}
boolean expanded = DBUtils.getExpanded( m_context, m_rowid );
update( expanded, m_summary.lastMoveTime, haveATurn,
haveALocalTurn );
}
}
private class LoadItemTask extends AsyncTask<Void, Void, GameSummary> {
@Override
protected GameSummary doInBackground( Void... unused )
{
return DBUtils.getSummary( m_context, m_rowid, 1500 );
} // doInBackground
@Override
protected void onPostExecute( GameSummary summary )
{
m_summary = summary;
setData();
// setLoaded( m_view.getRowID() );
setLoaded();
DbgUtils.logf( "LoadItemTask for row %d finished", m_rowid );
}
} // class LoadItemTask
}

View file

@ -200,8 +200,7 @@ public class GamesList extends XWListActivity
public void onClick( DialogInterface dlg, int item ) {
String name = namerView.getName();
DBUtils.setName( GamesList.this, m_rowid, name );
m_adapter.inval( m_rowid );
onContentChanged();
m_adapter.invalName( m_rowid );
}
};
dialog = new AlertDialog.Builder( this )
@ -283,7 +282,8 @@ public class GamesList extends XWListActivity
});
String field = CommonPrefs.getSummaryField( this );
m_adapter = new GameListAdapter( this, new Handler(), this, field );
m_adapter = new GameListAdapter( this, getListView(), new Handler(),
this, field );
setListAdapter( m_adapter );
NetUtils.informOfDeaths( this );
@ -381,12 +381,15 @@ public class GamesList extends XWListActivity
}
// DBUtils.DBChangeListener interface
public void gameSaved( final long rowid )
public void gameSaved( final long rowid, final boolean countChanged )
{
post( new Runnable() {
public void run() {
m_adapter.inval( rowid );
onContentChanged();
if ( countChanged ) {
onContentChanged();
} else {
m_adapter.inval( rowid );
}
}
} );
}
@ -457,7 +460,6 @@ public class GamesList extends XWListActivity
long[] games = DBUtils.gamesList( this );
for ( int ii = games.length - 1; ii >= 0; --ii ) {
GameUtils.deleteGame( this, games[ii], ii == 0 );
m_adapter.inval( games[ii] );
}
break;
case SYNC_MENU_ACTION:
@ -738,7 +740,6 @@ public class GamesList extends XWListActivity
}
}
}
onContentChanged();
}
}
@ -846,7 +847,9 @@ public class GamesList extends XWListActivity
{
String newField = CommonPrefs.getSummaryField( this );
if ( m_adapter.setField( newField ) ) {
onContentChanged();
// The adapter should be able to decide whether full
// content change is required. PENDING
onContentChanged();
}
}

View file

@ -42,13 +42,13 @@ public abstract class XWListAdapter implements ListAdapter {
public boolean isEnabled( int position ) { return true; }
public int getCount() { return m_count; }
public Object getItem( int position ) { return null; }
public long getItemId(int position) { return position; }
public long getItemId( int position ) { return position; }
public int getItemViewType( int position ) {
return ListAdapter.IGNORE_ITEM_VIEW_TYPE;
}
public int getViewTypeCount() { return 1; }
public boolean hasStableIds() { return true; }
public boolean isEmpty() { return getCount() == 0; }
public void registerDataSetObserver(DataSetObserver observer) {}
public void unregisterDataSetObserver(DataSetObserver observer) {}
public void registerDataSetObserver( DataSetObserver observer ) {}
public void unregisterDataSetObserver( DataSetObserver observer ) {}
}