rewrite list item logic. Use a single custom LinearLayout subclass

for both the loading and loaded phases, toggling its state once the
data's available.  Reuse it: pay attention to what's passed into
getView and only allocate when there's no existing View to reuse.
Stop caching Views, as that defeats Android list logic that might
limit in-memory representation to the subset that's visible on-screen,
instead tracking a set of rowids whose data is known to be good as a
way of quickly drawing when there's a refresh.
This commit is contained in:
Eric House 2012-12-08 08:47:53 -08:00
parent 83b1d4c364
commit 7efbd2697d
5 changed files with 420 additions and 324 deletions
xwords4/android/XWords4

View file

@ -3,8 +3,9 @@
<!-- top-level layout is hozontal, with an image and another layout -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
<org.eehouse.android.xw4.GameListItem
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:longClickable="true"
@ -13,6 +14,23 @@
android:background="@android:drawable/list_selector_background"
>
<TextView android:id="@+id/view_unloaded"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:gravity="center"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:text="@string/game_list_tmp"
/>
<LinearLayout android:id="@+id/view_loaded"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:visibility="gone"
>
<ImageView android:id="@+id/msg_marker"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
@ -94,4 +112,5 @@
/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</org.eehouse.android.xw4.GameListItem>

View file

@ -35,7 +35,7 @@ import android.widget.TextView;
import java.io.FileInputStream;
import java.text.DateFormat;
import java.util.Date;
import java.util.HashMap; // class is not synchronized
import java.util.HashSet;
import java.util.Random;
import junit.framework.Assert;
@ -46,10 +46,6 @@ import org.eehouse.android.xw4.jni.CurGameInfo.DeviceRole;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
public class GameListAdapter extends XWListAdapter {
private Context m_context;
private LayoutInflater m_factory;
private int m_fieldID;
private Handler m_handler;
private static final boolean s_isFire;
private static Random s_random;
static {
@ -59,79 +55,35 @@ public class GameListAdapter extends XWListAdapter {
}
}
private class ViewInfo implements View.OnClickListener {
private View m_view;
private View m_hideable;
private ExpiringTextView m_name;
private boolean m_expanded, m_haveTurn, m_haveTurnLocal;
private long m_rowid;
private long m_lastMoveTime;
private ImageButton m_expandButton;
public ViewInfo( View view, long rowid )
{
m_view = view;
m_rowid = rowid;
m_lastMoveTime = 0;
}
public ViewInfo( View view, long rowid, boolean expanded,
long lastMoveTime, boolean haveTurn,
boolean haveTurnLocal ) {
this( view, rowid );
m_expanded = expanded;
m_lastMoveTime = lastMoveTime;
m_haveTurn = haveTurn;
m_haveTurnLocal = haveTurnLocal;
m_hideable = (LinearLayout)view.findViewById( R.id.hideable );
m_name = (ExpiringTextView)m_view.findViewById( R.id.game_name );
m_expandButton = (ImageButton)view.findViewById( R.id.expander );
m_expandButton.setOnClickListener( this );
showHide();
}
private void showHide()
{
m_expandButton.setImageResource( m_expanded ?
R.drawable.expander_ic_maximized :
R.drawable.expander_ic_minimized);
m_hideable.setVisibility( m_expanded? View.VISIBLE : View.GONE );
m_name.setBackgroundColor( android.R.color.transparent );
m_name.setPct( m_handler, m_haveTurn && !m_expanded,
m_haveTurnLocal, m_lastMoveTime );
}
public void onClick( View view ) {
m_expanded = !m_expanded;
DBUtils.setExpanded( m_rowid, m_expanded );
showHide();
}
}
private HashMap<Long,ViewInfo> m_viewsCache;
private Context m_context;
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 itemLoaded( long rowid );
public void itemClicked( long rowid );
}
private class LoadItemTask extends AsyncTask<Void, Void, Void> {
private long m_rowid;
private class LoadItemTask extends AsyncTask<Void, Void, GameSummary> {
private GameListItem m_view;
private Context m_context;
// private int m_id;
public LoadItemTask( Context context, long rowid/*, int id*/ )
public LoadItemTask( Context context, GameListItem view )
{
DbgUtils.logf( "Creating LoadItemTask for row %d",
view.getRowID() );
m_context = context;
m_rowid = rowid;
// m_id = id;
m_view = view;
}
@Override
protected Void doInBackground( Void... unused )
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
@ -143,18 +95,143 @@ public class GameListAdapter extends XWListAdapter {
} catch ( Exception e ) {
}
}
View layout = m_factory.inflate( R.layout.game_list_item, null );
boolean hideTitle = false;//CommonPrefs.getHideTitleBar(m_context);
GameSummary summary = DBUtils.getSummary( m_context, m_rowid, 1500 );
if ( null == summary ) {
m_rowid = -1;
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 ) {
super( DBUtils.gamesList(context).length );
m_context = context;
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 );
}
@Override
public int getCount() {
return DBUtils.gamesList(m_context).length;
}
// Views. A view depends on a summary, which takes time to load.
// 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();
}
return result;
}
public void inval( long rowid )
{
synchronized( m_loadedRows ) {
m_loadedRows.remove( rowid );
}
}
private void dirtyAll()
{
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 );
}
}
public boolean setField( String fieldName )
{
boolean changed = false;
int newID = fieldToID( fieldName );
if ( -1 == newID ) {
if ( XWApp.DEBUG ) {
DbgUtils.logf( "GameListAdapter.setField(): unable to match"
+ " fieldName %s", fieldName );
}
} else if ( m_fieldID != newID ) {
if ( XWApp.DEBUG ) {
DbgUtils.logf( "setField: clearing views cache for change"
+ " from %d to %d", m_fieldID, newID );
}
m_fieldID = newID;
dirtyAll();
changed = true;
}
return changed;
}
private int fieldToID( String fieldName )
{
int[] ids = {
R.string.game_summary_field_empty
,R.string.game_summary_field_language
,R.string.game_summary_field_opponents
,R.string.game_summary_field_state
};
int result = -1;
for ( int id : ids ) {
if ( m_context.getString( id ).equals( fieldName ) ) {
result = id;
break;
}
}
return result;
}
private void setData( GameListItem layout, GameSummary summary )
{
if ( null != summary ) {
final long rowid = layout.getRowID();
String state = summary.summarizeState();
TextView view = (TextView)layout.findViewById( R.id.game_name );
if ( hideTitle ) {
view.setVisibility( View.GONE );
} else {
String value = null;
switch ( m_fieldID ) {
case R.string.game_summary_field_empty:
@ -172,7 +249,7 @@ public class GameListAdapter extends XWListAdapter {
break;
}
String name = GameUtils.getName( m_context, m_rowid );
String name = GameUtils.getName( m_context, rowid );
if ( null != value ) {
value = m_context.getString( R.string.str_game_namef,
@ -182,12 +259,11 @@ public class GameListAdapter extends XWListAdapter {
}
view.setText( value );
}
layout.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick( View v ) {
m_cb.itemClicked( m_rowid );
m_cb.itemClicked( rowid );
}
} );
@ -245,123 +321,10 @@ public class GameListAdapter extends XWListAdapter {
view.setVisibility( View.GONE );
}
boolean expanded = DBUtils.getExpanded( m_context, m_rowid );
ViewInfo vi = new ViewInfo( layout, m_rowid, expanded,
summary.lastMoveTime, haveATurn,
haveALocalTurn );
if ( XWApp.DEBUG ) {
DbgUtils.logf( "created new view for rowid %d", m_rowid );
}
synchronized( m_viewsCache ) {
m_viewsCache.put( m_rowid, vi );
}
}
return null;
} // doInBackground
boolean expanded = DBUtils.getExpanded( m_context, rowid );
@Override
protected void onPostExecute( Void unused )
{
// DbgUtils.logf( "onPostExecute(rowid=%d)", m_rowid );
if ( -1 != m_rowid ) {
m_cb.itemLoaded( m_rowid );
layout.update( m_handler, expanded, summary.lastMoveTime,
haveATurn, haveALocalTurn );
}
}
} // class LoadItemTask
public GameListAdapter( Context context, Handler handler, LoadItemCB cb ) {
super( DBUtils.gamesList(context).length );
m_context = context;
m_handler = handler;
m_cb = cb;
m_factory = LayoutInflater.from( context );
m_df = DateFormat.getDateTimeInstance( DateFormat.SHORT,
DateFormat.SHORT );
m_viewsCache = new HashMap<Long,ViewInfo>();
}
public int getCount() {
return DBUtils.gamesList(m_context).length;
}
public Object getItem( int position )
{
final long rowid = DBUtils.gamesList(m_context)[position];
View layout;
boolean haveLayout = false;
synchronized( m_viewsCache ) {
ViewInfo vi = m_viewsCache.get( rowid );
haveLayout = null != vi;
if ( haveLayout ) {
layout = vi.m_view;
} else {
layout = m_factory.inflate( R.layout.game_list_tmp, null );
vi = new ViewInfo( layout, rowid );
m_viewsCache.put( rowid, vi );
}
}
if ( !haveLayout ) {
new LoadItemTask( m_context, rowid/*, ++m_taskCounter*/ ).execute();
}
// this doesn't work. Rather, it breaks highlighting because
// the background, if we don't set it, is a more complicated
// object like @android:drawable/list_selector_background. I
// tried calling getBackground(), expecting to get a Drawable
// I could then clone and modify, but null comes back. So
// layout must be inheriting its background from elsewhere or
// it gets set later, during layout.
// if ( (position%2) == 0 ) {
// layout.setBackgroundColor( 0xFF3F3F3F );
// }
return layout;
} // getItem
public View getView( int position, View convertView, ViewGroup parent ) {
return (View)getItem( position );
}
public void inval( long rowid )
{
synchronized( m_viewsCache ) {
m_viewsCache.remove( rowid );
}
}
public boolean setField( String fieldName )
{
boolean changed = false;
int[] ids = {
R.string.game_summary_field_empty
,R.string.game_summary_field_language
,R.string.game_summary_field_opponents
,R.string.game_summary_field_state
};
int result = -1;
for ( int id : ids ) {
if ( m_context.getString( id ).equals( fieldName ) ) {
result = id;
break;
}
}
if ( -1 == result ) {
if ( XWApp.DEBUG ) {
DbgUtils.logf( "GameListAdapter.setField(): unable to match"
+ " fieldName %s", fieldName );
}
} else if ( m_fieldID != result ) {
if ( XWApp.DEBUG ) {
DbgUtils.logf( "setField: clearing views cache for change"
+ " from %d to %d", m_fieldID, result );
}
m_viewsCache.clear();
m_fieldID = result;
changed = true;
}
return changed;
}
}

View file

@ -0,0 +1,115 @@
/* -*- compile-command: "cd ../../../../../; ant debug install"; -*- */
/*
* Copyright 2009-2012 by Eric House (xwords@eehouse.org). All rights
* reserved.
*
* 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 of the
* License, 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.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.eehouse.android.xw4;
import android.content.Context;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageButton;
import android.widget.LinearLayout;
public class GameListItem extends LinearLayout
implements View.OnClickListener {
private Context m_context;
private boolean m_loaded;
private long m_rowid;
private View m_hideable;
private ExpiringTextView m_name;
private boolean m_expanded, m_haveTurn, m_haveTurnLocal;
private long m_lastMoveTime;
private ImageButton m_expandButton;
private Handler m_handler;
public GameListItem( Context cx, AttributeSet as )
{
super( cx, as );
m_context = cx;
m_loaded = false;
m_rowid = DBUtils.ROWID_NOTFOUND;
m_lastMoveTime = 0;
}
public void update( Handler handler, boolean expanded,
long lastMoveTime, boolean haveTurn,
boolean haveTurnLocal )
{
m_handler = handler;
m_expanded = expanded;
m_lastMoveTime = lastMoveTime;
m_haveTurn = haveTurn;
m_haveTurnLocal = haveTurnLocal;
m_hideable = (LinearLayout)findViewById( R.id.hideable );
m_name = (ExpiringTextView)findViewById( R.id.game_name );
m_expandButton = (ImageButton)findViewById( R.id.expander );
m_expandButton.setOnClickListener( this );
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;
}
// View.OnClickListener interface
public void onClick( View view ) {
m_expanded = !m_expanded;
DBUtils.setExpanded( m_rowid, m_expanded );
showHide();
}
private void showHide()
{
m_expandButton.setImageResource( m_expanded ?
R.drawable.expander_ic_maximized :
R.drawable.expander_ic_minimized);
m_hideable.setVisibility( m_expanded? View.VISIBLE : View.GONE );
m_name.setBackgroundColor( android.R.color.transparent );
m_name.setPct( m_handler, m_haveTurn && !m_expanded,
m_haveTurnLocal, m_lastMoveTime );
}
}

View file

@ -282,7 +282,8 @@ public class GamesList extends XWListActivity
}
});
m_adapter = new GameListAdapter( this, new Handler(), this );
String field = CommonPrefs.getSummaryField( this );
m_adapter = new GameListAdapter( this, new Handler(), this, field );
setListAdapter( m_adapter );
NetUtils.informOfDeaths( this );
@ -391,11 +392,6 @@ public class GamesList extends XWListActivity
}
// GameListAdapter.LoadItemCB interface
public void itemLoaded( long rowid )
{
onContentChanged();
}
public void itemClicked( long rowid )
{
// We need a way to let the user get back to the basic-config

View file

@ -41,8 +41,11 @@ public abstract class XWListAdapter implements ListAdapter {
public boolean areAllItemsEnabled() { return true; }
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 int getItemViewType(int position) { return 0; }
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; }