Android: EfficientAdapter com duas visões diferentes
Pergunta
Eu estou usando uma versão estendida do BaseAdapter baseado no EfficientAdapter exemplo das amostras SDK demo.
Os meus dados é basicamente um objeto (ListPlaces
), que detém uma ArrayList
com a lista real de lugares, acessível através listPlaces.getValues()
. Estes dados ArrayList é ordenada por alcance ea ArrayList
consistem de alguns itens especiais (separadores), sem dados, mas um conjunto bandeira separator
para true
.
Agora, sempre que o meu EfficientAdapter
recebe um objeto de dados que é um separador ele retorna false
para inflar public boolean isEnabled(int position)
e public View getView(int position, View convertView, ViewGroup parent)
dois layouts diferentes dependendo se o objeto de dados atual consiste em dados reais ou é apenas um manequim separador.
Isso funciona muito bem, se eu inflar o layout de cada vez. No entanto, inflando o layout de cada vez e chamando findViewById
torna o ListView
quase unusabely lento.
Então, eu tentei usar o EfficientAdapter com abordagem ViewHolder
. Mas isso não funcionou direito da caixa, por causa dos dois pontos de vista diferentes tento acesso. Assim, sempre que o meu convertView != null
(a outra caso) acessa os itens no layout através do nosso ViewHolder
e quando a exibição anterior era um separador que, obviamente, não trabalho para o acesso de um TextView lá que está disponível apenas nos itens "reais" de layout .
Então, eu também forçar meu getView()
para inflar o layout não só quando convertView == null
, mas também quando o listRow anterior é diferente do que a atual: if (convertView == null || (listRow != listRow_previous)) { [....] }
Isto parece quase funcionar agora. Ou pelo menos não falhar desde o início. Mas ainda trava e eu não sei o que tenho de fazer diferente. Eu tentei olhar para convertView.getID()
e convertView.getResources()
, mas isso não foi realmente útil até agora. Talvez alguém tem uma idéia de como posso verificar se o meu convertView
atual corresponde com o layout item da lista ou o layout separador de lista. Obrigado.
Aqui está o código. Onde sempre há um [...] Eu tirei algum código menos importante para torná-lo mais fácil de ler e compreender:
private class EfficientAdapter extends BaseAdapter {
private LayoutInflater mInflater;
private ListPlaces listPlaces;
private ListRow listRow;
private ListRow listRow_previous;
public EfficientAdapter(Context context, ListPlaces listPlaces) {
// Cache the LayoutInflate to avoid asking for a new one each time.
mInflater = LayoutInflater.from(context);
// Data
this.listPlaces = listPlaces;
}
/**
* The number of items in the list is determined by the number of items
* in our ArrayList
*
* @see android.widget.ListAdapter#getCount()
*/
public int getCount() {
return listPlaces.getValues().size();
}
/**
* Since the data comes from an array, just returning the index is
* sufficent to get at the data. If we were using a more complex data
* structure, we would return whatever object represents one row in the
* list.
*
* @see android.widget.ListAdapter#getItem(int)
*/
public Object getItem(int position) {
return position;
}
/**
* Use the array index as a unique id.
*
* @see android.widget.ListAdapter#getItemId(int)
*/
public long getItemId(int position) {
return position;
}
@Override
public boolean isEnabled(int position) {
// return false if item is a separator:
if(listPlaces.getValues().get(position).separator >= 0)
return false;
else
return true;
}
@Override
public boolean areAllItemsEnabled() {
return false;
}
/**
* Make a view to hold each row.
*
* @see android.widget.ListAdapter#getView(int, android.view.View,
* android.view.ViewGroup)
*/
public View getView(int position, View convertView, ViewGroup parent) {
// Get the values for the current list element
ListPlacesValues curValues = listPlaces.getValues().get(position);
if (curValues.separator >= 0)
listRow = ListRow.SEPARATOR;
else
listRow = ListRow.ITEM;
Log.i(TAG,"Adapter: getView("+position+") " + listRow + " (" + listRow_previous + ") -> START");
// A ViewHolder keeps references to children views to avoid unneccessary calls
// to findViewById() on each row.
ViewHolder holder;
// When convertView is not null, we can reuse it directly, there is no need
// to reinflate it. We only inflate a new View when the convertView supplied
// by ListView is null.
if (convertView == null || (listRow != listRow_previous)) {
Log.i(TAG, "--> (convertView == null) at position: " + position);
// Creates a ViewHolder and store references to the two children views
// we want to bind data to.
holder = new ViewHolder();
if (listRow == ListRow.SEPARATOR) {
convertView = mInflater.inflate(R.layout.taxonomy_list_separator, null);
holder.separatorText = (TextView) convertView.findViewById(R.id.separatorText);
convertView.setTag(holder);
Log.i(TAG,"\tCREATE SEPARATOR: convertView ID: " + convertView.getId() + " Resource: " + convertView.getResources());
}
else {
convertView = mInflater.inflate(R.layout.taxonomy_listitem, null);
holder.name = (TextView) convertView.findViewById(R.id.name);
holder.category = (TextView) convertView.findViewById(R.id.category);
// [...]
convertView.setTag(holder);
Log.i(TAG,"\tCREATE ITEM: convertView ID: " + convertView.getId() + " Resource: " + convertView.getResources());
}
} else {
// Get the ViewHolder back to get fast access to the TextView
// and the ImageView.
Log.i(TAG,"\tconvertView ID: " + convertView.getId() + " Resource: " + convertView.getResources());
holder = (ViewHolder) convertView.getTag();
convertView.setAnimation(null);
}
/* Bind the data efficiently with the holder */
if (listRow == ListRow.SEPARATOR) {
String separatorText;
switch (curValues.separator) {
case 0: separatorText="case 0"; break;
case 1: separatorText="case 1"; break;
case 2: separatorText="case 2"; break;
// [...]
default: separatorText="[ERROR]"; break;
}
holder.separatorText.setText(separatorText);
}
else {
// Set the name:
holder.name.setText(curValues.name);
// Set category
String cat = curValues.classification.toString();
cat = cat.substring(1,cat.length()-1); // removing "[" and "]"
if (cat.length() > 35) {
cat = cat.substring(0, 35);
cat = cat + "...";
}
holder.category.setText(cat);
// [...] (and many more TextViews and ImageViews to be set)
}
listRow_previous = listRow;
Log.i(TAG,"Adapter: getView("+position+") -> DONE");
return convertView;
}
private class ViewHolder {
TextView name;
TextView category;
// [...] -> many more TextViews and ImageViews
TextView separatorText;
}
}
E aqui a minha Logcat de saída:
755 ListPlaces_Activity I onPostExecute: notifyDataSetChanged()
755 ListPlaces_Activity I Adapter: getView(0) SEPARATOR (null) -> START
755 ListPlaces_Activity I --> (convertView == null) at position: 0
755 ListPlaces_Activity I CREATE SEPARATOR: convertView ID: 2131296317 Resource: android.content.res.Resources@437613e0
755 ListPlaces_Activity I Adapter: getView(0) -> DONE
755 ListPlaces_Activity I Adapter: getView(1) ITEM (SEPARATOR) -> START
755 ListPlaces_Activity I --> (convertView == null) at position: 1
755 ListPlaces_Activity I CREATE ITEM: convertView ID: 2131296317 Resource: android.content.res.Resources@437613e0
755 ListPlaces_Activity I Adapter: getView(1) -> DONE
755 ListPlaces_Activity I Adapter: getView(2) SEPARATOR (ITEM) -> START
755 ListPlaces_Activity I --> (convertView == null) at position: 2
755 ListPlaces_Activity I CREATE SEPARATOR: convertView ID: 2131296317 Resource: android.content.res.Resources@437613e0
755 ListPlaces_Activity I Adapter: getView(2) -> DONE
755 ListPlaces_Activity I Adapter: getView(3) ITEM (SEPARATOR) -> START
755 ListPlaces_Activity I --> (convertView == null) at position: 3
755 ListPlaces_Activity I CREATE ITEM: convertView ID: 2131296317 Resource: android.content.res.Resources@437613e0
755 ListPlaces_Activity I Adapter: getView(3) -> DONE
755 ListPlaces_Activity I Adapter: getView(4) ITEM (ITEM) -> START
755 ListPlaces_Activity I convertView ID: 2131296317 Resource: android.content.res.Resources@437613e0
755 ListPlaces_Activity I Adapter: getView(4) -> DONE
755 ListPlaces_Activity I Adapter: getView(5) ITEM (ITEM) -> START
755 ListPlaces_Activity I convertView ID: 2131296317 Resource: android.content.res.Resources@437613e0
755 ListPlaces_Activity I Adapter: getView(5) -> DONE
755 ListPlaces_Activity I Adapter: getView(6) ITEM (ITEM) -> START
755 ListPlaces_Activity I convertView ID: 2131296317 Resource: android.content.res.Resources@437613e0
755 ListPlaces_Activity I Adapter: getView(6) -> DONE
755 ListPlaces_Activity I Adapter: getView(0) SEPARATOR (ITEM) -> START
755 ListPlaces_Activity I --> (convertView == null) at position: 0
755 ListPlaces_Activity I CREATE SEPARATOR: convertView ID: 2131296317 Resource: android.content.res.Resources@437613e0
755 ListPlaces_Activity I Adapter: getView(0) -> DONE
755 ListPlaces_Activity I Adapter: getView(1) ITEM (SEPARATOR) -> START
755 ListPlaces_Activity I --> (convertView == null) at position: 1
755 ListPlaces_Activity I CREATE ITEM: convertView ID: 2131296317 Resource: android.content.res.Resources@437613e0
755 ListPlaces_Activity I Adapter: getView(1) -> DONE
755 ListPlaces_Activity I Adapter: getView(2) SEPARATOR (ITEM) -> START
755 ListPlaces_Activity I --> (convertView == null) at position: 2
755 ListPlaces_Activity I CREATE SEPARATOR: convertView ID: 2131296317 Resource: android.content.res.Resources@437613e0
755 ListPlaces_Activity I Adapter: getView(2) -> DONE
755 ListPlaces_Activity I Adapter: getView(3) ITEM (SEPARATOR) -> START
755 ListPlaces_Activity I --> (convertView == null) at position: 3
755 ListPlaces_Activity I CREATE ITEM: convertView ID: 2131296317 Resource: android.content.res.Resources@437613e0
755 ListPlaces_Activity I Adapter: getView(3) -> DONE
755 ListPlaces_Activity I Adapter: getView(4) ITEM (ITEM) -> START
755 ListPlaces_Activity I convertView ID: 2131296317 Resource: android.content.res.Resources@437613e0
755 ListPlaces_Activity I Adapter: getView(4) -> DONE
755 ListPlaces_Activity I Adapter: getView(5) ITEM (ITEM) -> START
755 ListPlaces_Activity I convertView ID: 2131296317 Resource: android.content.res.Resources@437613e0
755 AndroidRuntime D Shutting down VM
755 dalvikvm W threadid=3: thread exiting with uncaught exception (group=0x4001aa28)
755 AndroidRuntime E Uncaught handler: thread main exiting due to uncaught exception
755 AndroidRuntime E java.lang.NullPointerException
755 AndroidRuntime E at com.tato.main.ListPlaces_Activity$EfficientAdapter.getView(ListPlaces_Activity.java:330)
755 AndroidRuntime E at android.widget.HeaderViewListAdapter.getView(HeaderViewListAdapter.java:191)
755 AndroidRuntime E at android.widget.AbsListView.obtainView(AbsListView.java:1255)
755 AndroidRuntime E at android.widget.ListView.makeAndAddView(ListView.java:1658)
755 AndroidRuntime E at android.widget.ListView.fillDown(ListView.java:637)
755 AndroidRuntime E at android.widget.ListView.fillFromTop(ListView.java:694)
755 AndroidRuntime E at android.widget.ListView.layoutChildren(ListView.java:1502)
755 AndroidRuntime E at android.widget.AbsListView.onLayout(AbsListView.java:1112)
755 AndroidRuntime E at android.view.View.layout(View.java:6569)
755 AndroidRuntime E at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1119)
755 AndroidRuntime E at android.widget.LinearLayout.layoutVertical(LinearLayout.java:998)
755 AndroidRuntime E at android.widget.LinearLayout.onLayout(LinearLayout.java:918)
755 AndroidRuntime E at android.view.View.layout(View.java:6569)
755 AndroidRuntime E at android.widget.FrameLayout.onLayout(FrameLayout.java:333)
755 AndroidRuntime E at android.view.View.layout(View.java:6569)
755 AndroidRuntime E at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1119)
755 AndroidRuntime E at android.widget.LinearLayout.layoutVertical(LinearLayout.java:998)
755 AndroidRuntime E at android.widget.LinearLayout.onLayout(LinearLayout.java:918)
755 AndroidRuntime E at android.view.View.layout(View.java:6569)
755 AndroidRuntime E at android.widget.FrameLayout.onLayout(FrameLayout.java:333)
755 AndroidRuntime E at android.view.View.layout(View.java:6569)
755 AndroidRuntime E at android.view.ViewRoot.performTraversals(ViewRoot.java:979)
755 AndroidRuntime E at android.view.ViewRoot.handleMessage(ViewRoot.java:1613)
755 AndroidRuntime E at android.os.Handler.dispatchMessage(Handler.java:99)
755 AndroidRuntime E at android.os.Looper.loop(Looper.java:123)
755 AndroidRuntime E at android.app.ActivityThread.main(ActivityThread.java:4203)
755 AndroidRuntime E at java.lang.reflect.Method.invokeNative(Native Method)
755 AndroidRuntime E at java.lang.reflect.Method.invoke(Method.java:521)
755 AndroidRuntime E at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:791)
755 AndroidRuntime E at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:549)
755 AndroidRuntime E at dalvik.system.NativeStart.main(Native Method)
Solução
Você esqueceu um par de métodos que você precisa para substituição: getViewTypeCount () e getItemViewType () . Estes não são necessários para listas onde todas as linhas são os mesmos, mas eles são muito importantes para o seu cenário. Implementar estas corretamente, e Android manterá pools de objetos separados para seus cabeçalhos e linhas de detalhes.
Ou, você pode olhar para:
- original de Jeff Sharkey SeparatedListAdapter (GPL)
- atualizados rendition (GPL)
- MergeAdapter , que pode ser utilizado para este cenário, bem como (Apache)
Outras dicas
Graças à sugestão com getViewTypeCount () e getItemViewType () ele funciona perfeitamente agora.
A implementação desses dois métodos foi muito simples:
@Override
public int getViewTypeCount() {
return 2;
}
@Override
public int getItemViewType(int position) {
if(listPlaces.getValues().get(position).separator >= 0)
return 0;
else
return 1;
}
Como commonsware mencionado na sua resposta desta forma Android manterá diferentes piscinas objeto para diferentes itens da lista, o que também significa que você pode remover a verificação de listRow_previous
no meu exemplo e mudar o if (convertView == null || (listRow != listRow_previous))
apenas if (convertView == null)
.