Мобильные приложения
Android: ListView, Adapter и различные layout элементов списка в одном ListView
Разрабатывая один проект, я обнаружил проблему утечки памяти в таком графическом элементе как ListView, а именно в адаптере (Adapter), который с ним связан. После нескольких минут поиска в google.com и stackoverflow.com я понял, что создавать каждый раз объект(ы) для convertView в методе
getView(int postion, View convertView, ViewGroup parent)
неграмотно, поскольку этот метод вызывается каждый раз, когда появляется новый элемент списка. Для того, чтобы этого избежать, достаточно ввести новый класс, например ViewHolder, и хранить в нем экземпляры нужных нам виджетов. Т.е. вместо следующего кода:
public View getView(int position, View convertView, ViewGroup parent) {
TextView text;
if (convertView == null) {
text = (TextView)mInflater.inflate(android.R.layout.simple_list_item_1, parent, false);
} else {
text = (TextView)convertView;
}
text.setText("Some text.");
return text;
}
нужно использовать следующий:
public View getView(int position, View convertView, ViewGroup parent) {
// ViewHolder содержит ссылки на виджеты для избежания ненужных вызовов метода findViewById()
// в каждом новом видимом элементе списка
ViewHolder holder;
// Если convertView не null, мы используем его напрямую, нет необходимости повторно
// вызывать inflate
if (convertView == null) {
convertView = mInflater.inflate(R.layout.list_item_icon_text, null);
holder = new ViewHolder();
holder.text = (TextView) convertView.findViewById(R.id.text);
holder.icon = (ImageView) convertView.findViewById(R.id.icon);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.text.setText("Some text.");
return convertView;
}
static class ViewHolder {
TextView text;
}
Но на этом я не остановился и решил понять механизм работы в принципе метода getView в частности и Adapter в общем. И моё любопытство было вознаграждено.
Итак, давайте подведём итоги, что мы уже уяснили:
- ListView для каждого элемента списка вызывает метод getView у связанного с ним Adapter
- Новый View создается и отображается
А что, если наших элементов списка будет не 10 и не 100, а скажем 1 000 000 000. Создавать новый View для каждого из элементов? Это было бы глупо, и я был уверен, что Google на это не способен. На самом деле Android кэширует графические виджеты для нас, т.е. объекты типа View. Есть компонент в Android, который называется «Recycler». На просторах я нашел картинку, иллюстрирующую его работу:
Если вы имеете 1 миллиард записей — только видимые элементы будут в памяти + виджеты в Recycler. ListView вызывает для виджета типа type1 метод getView столько раз, сколько элементов отображается при первой загрузке. createView — null в методе getView и поэтому создается новый виджет типа type1 и возвращает его. ListView запрашивает виджет типа type1, когда один из виджетов данного типа за пределами видимости, и новый элемент такого же типа появляется снизу. convertView уже не null и равняется item1. Вам достаточно установить новое значение и вернуть convertView. Уже не нужно создавать заново виджет.
Давайте посмотрим, что выведет в консоль данный код:
public class MultipleItemsList extends ListActivity {
private MyCustomAdapter mAdapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAdapter = new MyCustomAdapter();
for (int i = 0; i < 50; i++) {
mAdapter.addItem("item " + i);
}
setListAdapter(mAdapter);
}
private class MyCustomAdapter extends BaseAdapter {
private ArrayList mData = new ArrayList();
private LayoutInflater mInflater;
public MyCustomAdapter() {
mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
public void addItem(final String item) {
mData.add(item);
notifyDataSetChanged();
}
@Override
public int getCount() {
return mData.size();
}
@Override
public String getItem(int position) {
return mData.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
System.out.println("getView " + position + " " + convertView);
ViewHolder holder = null;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.item1, null);
holder = new ViewHolder();
holder.textView = (TextView)convertView.findViewById(R.id.text);
convertView.setTag(holder);
} else {
holder = (ViewHolder)convertView.getTag();
}
holder.textView.setText(mData.get(position));
return convertView;
}
}
public static class ViewHolder {
public TextView textView;
}
}
Запустив программу, мы увидим следующее
Метод getView вызовется 9 раз и все эти разы convertView будет равен null.
INFO/System.out(947): getView 0 null INFO/System.out(947): getView 1 null INFO/System.out(947): getView 2 null INFO/System.out(947): getView 3 null INFO/System.out(947): getView 4 null INFO/System.out(947): getView 5 null INFO/System.out(947): getView 6 null INFO/System.out(947): getView 7 null INFO/System.out(947): getView 8 null
Прокрутим слегка вниз до тех пор, пока не появится item10:
convertView до сих пор null, потому что в Recycler нет виджета, т.к. граница item1 все ещё видна наверху.
INFO/System.out(947): getView 9 null
Давайте прокрутим список ещё:
Ура! convertView уже не null: item1 скрылся из области видимости прямиком в Recycler и item11 создалось на основе item1.
INFO/System.out(947): getView 10 android.widget.LinearLayout@437430f8
Давайте прокрутим ещё вниз, просто чтобы проверить, что случится:
INFO/System.out(947): getView 11 android.widget.LinearLayout@437447d0 INFO/System.out(947): getView 12 android.widget.LinearLayout@43744ff8 INFO/System.out(947): getView 13 android.widget.LinearLayout@43743fa8 INFO/System.out(947): getView 14 android.widget.LinearLayout@43745820 INFO/System.out(947): getView 15 android.widget.LinearLayout@43746048 INFO/System.out(947): getView 16 android.widget.LinearLayout@43746870 INFO/System.out(947): getView 17 android.widget.LinearLayout@43747098 INFO/System.out(947): getView 18 android.widget.LinearLayout@437478c0 INFO/System.out(947): getView 19 android.widget.LinearLayout@43748df0 INFO/System.out(947): getView 20 android.widget.LinearLayout@437430f8
convertView, как мы и ожидали, не null.
Теперь давайте посмотрим, что произойдет, если мы добавим сепаратор где-нибудь в списке. Вы должны сделать следующее
- Переопределить метод getViewTypeCount() – возвращает сколько различных layout будет использоваться
- Переопределить метод getItemViewType(int) – возвращает id типа по его позиции
- Сослаться convertView на нужный виджет в методе getView в зависимости от типа
Вот пример кода:
public class MultipleItemsList extends ListActivity {
private MyCustomAdapter mAdapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAdapter = new MyCustomAdapter();
for (int i = 1; i < 50; i++) {
mAdapter.addItem("item " + i);
if (i % 4 == 0) {
mAdapter.addSeparatorItem("separator " + i);
}
}
setListAdapter(mAdapter);
}
private class MyCustomAdapter extends BaseAdapter {
private static final int TYPE_ITEM = 0;
private static final int TYPE_SEPARATOR = 1;
private static final int TYPE_MAX_COUNT = TYPE_SEPARATOR + 1;
private ArrayList mData = new ArrayList();
private LayoutInflater mInflater;
private TreeSet mSeparatorsSet = new TreeSet();
public MyCustomAdapter() {
mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
public void addItem(final String item) {
mData.add(item);
notifyDataSetChanged();
}
public void addSeparatorItem(final String item) {
mData.add(item);
mSeparatorsSet.add(mData.size() - 1);
notifyDataSetChanged();
}
@Override
public int getItemViewType(int position) {
return mSeparatorsSet.contains(position) ? TYPE_SEPARATOR : TYPE_ITEM;
}
@Override
public int getViewTypeCount() {
return TYPE_MAX_COUNT;
}
@Override
public int getCount() {
return mData.size();
}
@Override
public String getItem(int position) {
return mData.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
int type = getItemViewType(position);
System.out.println("getView " + position + " " + convertView + " type = " + type);
if (convertView == null) {
holder = new ViewHolder();
switch (type) {
case TYPE_ITEM:
convertView = mInflater.inflate(R.layout.item1, null);
holder.textView = (TextView)convertView.findViewById(R.id.text);
break;
case TYPE_SEPARATOR:
convertView = mInflater.inflate(R.layout.item2, null);
holder.textView = (TextView)convertView.findViewById(R.id.textSeparator);
break;
}
convertView.setTag(holder);
} else {
holder = (ViewHolder)convertView.getTag();
}
holder.textView.setText(mData.get(position));
return convertView;
}
}
public static class ViewHolder {
public TextView textView;
}
}
Каждый 5-ый элемент будет сепаратором.
В логах мы увидим следующее:
INFO/System.out(1035): getView 0 null type = 0 INFO/System.out(1035): getView 1 null type = 0 INFO/System.out(1035): getView 2 null type = 0 INFO/System.out(1035): getView 3 null type = 0 INFO/System.out(1035): getView 4 null type = 1 INFO/System.out(1035): getView 5 null type = 0 INFO/System.out(1035): getView 6 null type = 0 INFO/System.out(1035): getView 7 null type = 0 INFO/System.out(1035): getView 8 null type = 0 INFO/System.out(1035): getView 9 null type = 1
Пролистнем список и посмотрим, что случится:
INFO/System.out(1035): getView 10 null type = 0 INFO/System.out(1035): getView 11 android.widget.LinearLayout@43744528 type = 0 INFO/System.out(1035): getView 12 android.widget.LinearLayout@43744eb0 type = 0 INFO/System.out(1035): getView 13 android.widget.LinearLayout@437456d8 type = 0 INFO/System.out(1035): getView 14 null type = 1 INFO/System.out(1035): getView 15 android.widget.LinearLayout@43745f00 type = 0 INFO/System.out(1035): getView 16 android.widget.LinearLayout@43747170 type = 0 INFO/System.out(1035): getView 17 android.widget.LinearLayout@43747998 type = 0 INFO/System.out(1035): getView 18 android.widget.LinearLayout@437481c0 type = 0 INFO/System.out(1035): getView 19 android.widget.LinearLayout@437468a0 type = 1 INFO/System.out(1035): getView 20 android.widget.LinearLayout@437489e8 type = 0 INFO/System.out(1035): getView 21 android.widget.LinearLayout@4374a8d8 type = 0
convertView не null до тех пор, пока первый сепаратор не скроется. Когда же он скрывается, то автоматически добавляется в Recycler и используется для дальнейших виджетов.




