понедельник, 5 июля 2010 г.

Адаптеры в Android

Давно я не занималась андроидом, но теперь, в связи с обретением девайса, намерена возобновить свои изыскания.
Нынешняя статья будет посвящена такой важной теме, как адаптеры. Ведь каждому, кому приходится сталкиваться с ListView или Spinner или там ListActivity волей-неволей приходится с ними разбираться.

Итак, адаптеры. Это прослойка между контролом, реализующим интерфейс AdapterView, и данными, которые отображаются в этом контроле. С помощью адаптеров можно настраивать способ отображения данных. Концепция весьма похожа на датабиндинг в WPF, хоть и не столь изящна.
В статье мы рассмотрим стандартные адаптеры, а также напишем собственный.

AdapterView

Как уже говорилось, есть такой интерфейс AdapterView, который реализуют все контролы, получающие данные для отображения от адаптеров. Контролов таких немного:
  • ListView — список
  • GridView — таблица
  • Gallery — галерея
  • Spinner — так почему-то называется выпадающий список
  • ExpandableListView — группированный список
Кроме контролов, адаптеры используются в некоторых активностях. А именно, в ListActivity и в прозводных от нее — PreferenceActivity и LauncherActivity

Стандартные адаптеры

Принцип работы у всех адаптеров похож. Есть набор объектов, и есть View. И есть правило, в соответствии с которым каждый объект биндится на этот View. A то, что получилось, добавляется в соответствующий AdapterView.

SimpleAdapter

Адаптер, позволяющий задавать отображение данных на разметку. Стоит рассмотреть его подробнее, так как с его помощью можно решать очень многие задачи. Кроме того, похожим образом работает еще ряд адаптеров. Для начала разберемся, как его определять. Конструктор имеет следующий вид:
SimpleAdapter(Context context, List<? extends Map<String, ?>> data, int resource, String[] from, int[] to)
Здесь вот что происходит. Данные, которые мы хотим отобразить, хранятся в списке data, каждый элемент которого — хеш, содержащий пары название свойства - значение. Каждый такой хеш биндится на разметку, идентификатор которой задается в параметре resource. Причем биндится не абы как: значение хеша с ключом from[i] отображается в элементе с идентификатором to[i].
Из подобной схемы работы очевидно, что в массиве from должны содержаться только те значения, которые присутствуют в ключах всех хешей, а также, что в массиве to должно не больше элементов, чем в from. Невыполнение этих условий приведет к ошибкам.
Рассмотрим пример. В некоторой активности выведем список сенсоров устройства.

SimpleAdapterActivity.java

public class SimpleAdapterActivity extends Activity
{
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.listview);
        
        SimpleAdapter adapter = new SimpleAdapter(this, createSensorsList(), android.R.layout.simple_list_item_2, 
            new String[] {"title", "vendor"}, 
            new int[] {android.R.id.text1, android.R.id.text2});
        
        ListView lv = (ListView)findViewById(R.id.list);
        lv.setAdapter(adapter);
    }    
    
    private List<Map<String, ?>> createSensorsList()
    {
        SensorManager sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
        List<Sensor> sensors = sensorManager.getSensorList(Sensor.TYPE_ALL);
        
        List<Map<String, ?>> items = new ArrayList<Map<String, ?>>();
        
        for (Sensor s : sensors)
        {
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("title", s.getName());
            map.put("vendor", s.getVendor());
            items.add(map);
        }
        
        return items;
    }
}
Результат работы такого кода на Desire:
Список сенсоров в SimpleAdapter

Пара слов о волшебных разметках

В приведенном примере в качестве идентификатора разметки передана загадочная константа android.R.layout.simple_list_item_2. Помнится, я, увидев такое в первый раз в AppDemos, не заметила слово android в начале и довольно долго пыталась понять, что это такое и откуда взялось. А разгадка проста. В SDK поставляется набор стандартных разметок на разные случаи жизни. Довольно часто используется android.R.layout.simple_list_item_1, представляющий собой простой TextView с идентификатором text1, а также наш android.R.layout.simple_list_item_2, содержащий заголовок с подзаголовком с идентификаторами соответственно text1 и text2.
Правда, есть у всех этих разметок одна проблема — в них так просто не заглянешь. Непосредственно в SDK, которое мы ставим для разработки, их нет, а в документации перечислены только значения констант, от коих толку мало. Так что, если захочется ознакомиться с содержимым стандартной разметки, придется брать git и сливать код отсюда: git://android.git.kernel.org/platform/frameworks/base.git. В полученном коде, в папке ~\core\res\res\layout можно найти все, что нужно.

SimpleAdapter.ViewBinder

А теперь попробуем добавить в разметку что-нибудь отличное от TextView. К примеру, SeekBar. Пусть будет вот такая разметка:

sensor_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="wrap_content">
 <TwoLineListItem xmlns:android="http://schemas.android.com/apk/res/android" 
    android:paddingTop="2dip"
    android:paddingBottom="2dip"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:minHeight="?android:attr/listPreferredItemHeight"
    android:mode="twoLine"
>
    
    <TextView android:id="@id/title"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="6dip"
        android:layout_marginTop="6dip"
        android:textAppearance="?android:attr/textAppearanceLarge"
    />
        
    <TextView android:id="@id/content"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/title"
        android:layout_alignLeft="@id/title"
        android:textAppearance="?android:attr/textAppearanceSmall"
    />

</TwoLineListItem>

<SeekBar android:id="@id/range" android:layout_width="fill_parent" android:layout_height="wrap_content" android:max="100" />

</LinearLayout>
Активность же изменим следующим образом:

ViewBinderActivity.java

public class ViewBinderActivity extends Activity
{
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.listview);

        SimpleAdapter adapter = new SimpleAdapter(this, createSensorsList(), R.layout.sensor_layout,                 new String[] {"title", "vendor", "power"},                 new int[] {R.id.title, R.id.content, R.id.range});
        ListView lv = (ListView)findViewById(R.id.list);         lv.setAdapter(adapter);     }          private List<Map<String, ?>> createSensorsList()     {         SensorManager sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);         List<Sensor> sensors = sensorManager.getSensorList(Sensor.TYPE_ALL);                  List<Map<String, ?>> items = new ArrayList<Map<String, ?>>();                  for (Sensor s : sensors)         {             Map<String, Object> map = new HashMap<String, Object>();             map.put("title", s.getName());             map.put("vendor", s.getVendor());             map.put("power", Math.round(s.getPower() * 10));             items.add(map);         }         return items;     } }
Запускаем — и приложение торжественно падает. Что же не так?
А дело вот в чем. Наш SimpleAdapter умеет биндить только TextView, ImageView и контролы, реализующие Checkable. Для всего остального нужно писать костыль под названием ViewBinder.
ViewBinder — внутренний интерфейс класса SimpleAdapter. У него есть всего один метод — setViewValue(android.view.View, Object, String), в котором принимается решение, какие поля контрола и как заполнять. Биндер для нашего случая может выглядеть, например, так:
private static class SeekBarBinder implements SimpleAdapter.ViewBinder
{
    public boolean setViewValue(View view, Object data,    String textRepresentation)
    {
        if (view.getId() == R.id.range)
        {
            ((SeekBar)view).setProgress((Integer)data);
            return true;
        }

        return false;
    }    
}
Кроме того, после инициализации адаптера нужно будет написать:
adapter.setViewBinder(new SeekBarBinder());
Вот теперь все красиво:
Результат прекрасной работы ViewBinder-а
Отмечу, что логика работы SimpleAdapter такова, что он сначала применяет ViewBinder, если он определен, затем, если вернулся false, пытается забиндить Checkable, потом TextView, потом ImageView. Так что ViewBinder — хорошее решение для случаев, когда нужно забиндить что-нибудь эдакое.

ArrayAdapter<T>

Адаптер, поставляющий контролу список элементов произвольного типа. Каждый элемент списка приводится к строке, и то, что получилось, выводится в TextView.
Рассмотрим сигнатуры конструкторов:
  • ArrayAdapter(Context context, int resource, int textViewResourceId)
  • ArrayAdapter(Context context, int resource, int textViewResourceId, T[] objects)
  • ArrayAdapter(Context context, int resource, int textViewResourceId, List<T> objects)
  • ArrayAdapter(Context context, int resource)
  • ArrayAdapter(Context context, int resource, T[] objects)
  • ArrayAdapter(Context context, int resource, List<T> objects)

NB:

В документации кое-где перепутаны обозначения. В последних трех сигнатурах вместо resource написали textViewResourceId.
Основные параметры адаптера:
  • context — текущий контекст.
  • resource — идентификатор разметки, которая будет применяться к элементам списка. Эта разметка должна содержать хотя бы один TextView. Обязательный параметр.
  • textViewResourceId — идентификатор TextView, к которому будут биндиться элементы списка. Если в разметке всего один TextView, этот параметр можно не указывать. Если же их несколько, то указывать нужно обязательно, иначе вылезет ошибка.
  • objects — список (или массив) отображаемых объектов.
Пример использования:

ArrayAdapterActivity.java

public class ArrayAdapterActivity extends Activity
{
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.listview);
        
        String[] strings = new String[] { "One", "Two", "Three" };
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, android.R.id.text1, strings);

        ListView list = (ListView)this.findViewById(R.id.list);
        list.setAdapter(adapter);
    }
}
Получим:
Пример работы ArrayAdapter<String>
Однако, хоть адаптер и громко называется ArrayAdapter<T>, но биндить таким образом объекты произвольного класса оказывается бессмысленным, потому что получается примерно следующее:
ArrayAdapter<SomeClass>
В виденных мной примерах ArrayAdapter используется либо для строк, либо для объектов классов с перегруженным toString(). В общем-то, это тоже вариант, потому что код получается менее громоздким, чем в случае SimpleAdapter. Но лично мне не нравится, что приходится в класс с данными добавлять костыли для корректной работы его отображения.

CursorAdapter

Адаптер, использующий в качестве источника данных курсор. Курсоры в андроиде — это отдельная история, достойная отдельной статьи. Вкратце, это инструмент для получения доступа к результатам некоего запроса. Это может быть запрос к БД, либо обращение к внутреннему хранилищу неких данных в телефоне (контактов, звонков и другого). Базы мы рассматривать не будем, т.к. по ним и так статей изрядно. Выведем список контактов, имеющихся в телефоне.
Так как класс CursorAdapter является абстрактным, будем использовать одну из его реализаций — SimpleCursorAdapter.

CursorAdapterActivity.java

public class CursorAdapterActivity extends Activity
{
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.cursor_adapter);

        String[] projection = new String[] { ContactsContract.Contacts.DISPLAY_NAME };

        Cursor cursor = managedQuery(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);

        SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, 
                android.R.layout.simple_list_item_1, 
                cursor, 
                new String[] { ContactsContract.Contacts.DISPLAY_NAME }, 
                new int[] { android.R.id.text1 });
        
        AdapterView view = (AdapterView)findViewById(R.id.contacts_list);
        view.setAdapter(adapter);
    }
}
В общем-то тут все вполне аналогично SimpleAdapter, только в качестве источника данных используется не список, а курсор.
Скажу пару слов про то, как мы здесь создавали курсор. Курсоры создаются с помощью метода query класса ContentResolver или метода managedQuery активности. Сигнатуры обоих методов одинаковы. А разница в том, что в первом случае нужно будет вручную писать код для закрытия и уничтожения курсора, а во втором этого не нужно — активность сама управляет курсором.

NB:

Для корректной работы данного примера в приложении должно быть выставлено разрешение на чтение контактов. Так что идем в раздел Permissions манифеста и добавляем там Uses Permission под названием android.permission.READ_CONTACTS.

Адаптеры для ExpandableList

Думаю, стоит отдельно рассмотреть, как используются адаптеры в группированных списках.
Напомню (или, может быть, сообщу), что ExpandableList состоит из групп, в каждой из которых содержится список. Соответственно, адаптер должен предоставлять все эти данные. В интерфейсе ExpandableListAdapter для этого определен ряд функций:
  • getGroupCount() — получить количество групп.
  • getGroup(int groupPosition) — получить объект, содержащийся в группе с заданным номером.
  • getChildrenCount(int groupPosition) — получить количество элементов в группе с заданным номером.
  • getChild(int groupPosition, int childPosition) — получить объект, содержащийся на заданном месте в списке у группы с заданным номером (во как!).
Однако пример полной реализации ExpandableListAdapter я приводить не буду. Как нетрудно догадаться, существует стандартный адаптер SimpleExpandableListAdapter, которым можно обойтись во многих случаях.
Посмотрим на один из конструкторов SimpleExpandableListAdapter:
SimpleExpandableListAdapter(Context context, 
    List<? extends Map<String, ?>> groupData, 
    int         groupLayout, 
    String[]    groupFrom, 
    int[]       groupTo,
    List<? extends List<? extends Map<String, ?>>> childData, 
    int         childLayout, 
    String[]    childFrom, 
    int[]       childTo)
В остальных конструкторах то же самое, только еще можно задавать отдельные разметки для свернутой или развернутой группы. Несложно заметить, что тут, в общем-то, то же самое, что и в SimpleAdapter, только в двойном экземпляре. Сначала параметры groupData, groupLayout, groupFrom и groupTo используются для привязки групп (назначение параметров точно то же, что и в SimpleAdapter). А потом для каждой i-й группы биндится ее выпадающий список, находящийся в childData[i], с аналогичным использованием параметров childLayout, childFrom и childTo. В общем, тут все только выглядит страшно, а на самом деле все вполне просто и логично.
Приведу пример:

SimpleExpandableListAdapterActivity.java

public class SimpleExpandableListAdapterActivity extends ExpandableListActivity
{
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        
        SimpleExpandableListAdapter adapter = new SimpleExpandableListAdapter(
                this, 
                createGroups(), 
                R.layout.display_header, 
                new String[] { "name" }, 
                new int[] { R.id.header1 },
                createChildren(), 
                R.layout.display_item, 
                new String[] { "name" }, 
                new int[] { R.id.text1 });
        
        setListAdapter(adapter);
    }    

    public List<Map<String, ?>> createGroups()  
    {
        List<Map<String, ?>> list = new ArrayList<Map<String, ?>>();
        
        for (int i = 1; i <= 3; ++i)
        {
            Map<String, Object> item = new HashMap<String, Object>();
            item.put("name", String.format("%d-th", i * 10));
            list.add(item);
        }
        
        return list;
    }

    public List<List<Map<String, ?>>> createChildren()
    {
        List<List<Map<String, ?>>> list = new ArrayList<List<Map<String,?>>>();
        
        for (int i = 1; i <= 3; ++i)
        {
            List<Map<String, ?>> itemList = new ArrayList<Map<String, ?>>();
            
            for (int j = 0; j < 5; ++j)
            {
                Map<String, Object> item = new HashMap<String, Object>();
                item.put("name", String.format("%d", i * 10 + j));
                itemList.add(item);
            }
            
            list.add(itemList);
        }
        
        return list;
    }
}
И результат:
Результат работы SimpleExpandableListAdapter
А еще есть SimpleCursorTreeAdapter — абстрактный класс, унаследовавшись от которого и определив всего одну функцию, можно заполнить раскрывающийся список из курсоров. Я уже не буду приводить здесь пример, но в исходниках к статье он есть.

Заключение

Итак, в статье мы дали определение адаптерам и расмотрели некоторые из стандартных адаптеров.
Исходники примера

23 комментария:

AT комментирует...

А что за девайс, если не секрет?
Просто тоже подумываю купить себе чего-нибудь.
Думал про дешевый SE XPeria X10 Mini, но он уж очень дешёвый. Это-то и настараживает.

Анонимный комментирует...

я себе на прошлой неделе Nexus One взял, в основном ради возможности халло ворд накодить, ибо к джаве теплые чувства ипытываю.

кстати интересно, это в последней версии SDK все еще нужны костыли по типу ViewBinder? как то несолидно... в свое время меня жутко расстроило кривое поведение GTK+ в мультипоточных приложениях (в исходниках LinuxDCPP таких кошмаров насмотрелся), а теперь такое и от гугля.... абидна (

darja комментирует...

2 AT:
HTC Desire

Анонимный комментирует...

Поражает количество кода для создания списков разных типов объектов, то бишь адаптеров создания списков. На самом деле объектов всего два - текст и изображение, тот же SeekBar - это всего лишь специализированное изображение, надо просто обновлять изображение в списке по требованию. Всё построено задом наперёд, не как сервисы, то есть список должен просить сервис (элемент списка) отрисоваться на своём Canvas, а элемент списка рисует на своём Canvas, а затем список копирует себе

Timur комментирует...

А что подразумевается под собственным адаптером?! Это пример, где расширяется SimpleAdapter?!

Dima L. комментирует...

То что искал, человеческим языком!
Дарья Вы молодец! :)

Анонимный комментирует...

Дашенька, Вы такая умница. Успехов Вам!

Unknown комментирует...

Есть ли возможность реализовать многоуровневую группировку?
Например: Группа->Подгруппа->Подподгруппа->елемент

Анонимный комментирует...

Может кто подскажет как заставить обновиться ListView при изменении данных используя SimpleAdapter. C ArrayAdapter все хорошо, а с этим приходится каждый раз вызывать ListView.setAdapter()

Анонимный комментирует...

Здравствуйте Дарья, вы не могли бы куда- нибудь залить файлы, связанные со стандартными разметками, просто по адресу "http://android.git.kernel.org/platform/frameworks/base.git/" отсутствует "~\core\res\res\layout"...

darja комментирует...

Анонимный #3:
Возможно, Вам поможет метод адаптера notifyDataSetChanged()

Анонимный #4:
Не вижу в этом необходимости. По этому адресу расположен Git-репозиторий, а вовсе не исходники. А чтобы получить исходники, Вам нужно установить git и выполнить команду git clone <url> [<path>]

Анонимный комментирует...

Вопрос не совсем по теме, но всё же.
Работаю с жестами. Когда создаю отдельную Activity, то всё нормально.
Потом копирую код в уже имеющеюсь главную Activity.
При этом выскакивает ошибка на строчке
mLibrary = GestureLibraries.fromRawResource(this, R.raw.gesture);

The method fromRawResource(Context, int) in the type GestureLibraries is not applicable for the arguments (SnakeView, int)

Помогите, пожалуйста.

Руслан комментирует...

подскажите пожалуйста, я только начал свое знакомство с Android и не могу понять вот этой конструкции List>, не подскажите что это за такой тип возвращаемый!

darja комментирует...

2 Руслан:
Это типизированная коллекция. Гуглите про Java generics.

Анонимный комментирует...

Дарья, огромное спасибо за руководство! Оно мне очень помогло!
Но, возникла небольшая трудность, в пример с SimpleExpandableListAdapter к хэдерам я добавил чекбоксы, и теперь у меня перестало работать выделение записей и раскрытие групп. Подскажите пожалуйста где может быть ошибка и возможно ли вообще такое сделать?

Анонимный комментирует...

А если применить стандартные разметки: R.layout.display_header заменить на android.R.layout.simple_list_item_multiple_choice, и R.id.header1 на android.R.id.text1, то группы раскрываются, но теперь не работают чекбоксы.

Анонимный комментирует...

Добавил к разметке чекбокса android:focusable="false", стали работать и раскрытия групп и проставление галочек, но теперь наблюдается странная картина: при раскрытии и закрытии групп галочки в чекбоксах симметрично перепрыгивают в чужие чекбоксы)

Анонимный комментирует...

Моя цель сделать чтобы чекбоксы в хэдерах групп проставлялись независимо от раскрытия групп
(в самих группах чекбоксов нет)
Я сделал:
listView.setChoiceMode(ExpandableListView.CHOICE_MODE_MULTIPLE);
listView.setOnGroupClickListener(cl);
}
private OnGroupClickListener...
public boolean onGroupClick...{

int position = groupPosition;
if (getExpandableListView().isItemChecked(position)) {
getExpandableListView().setItemChecked(position, false);
}
else {
getExpandableListView().setItemChecked(position, true);
}
но в этом случае чекбоксы меняют состояние синхронно с раскрытием своих групп,
и при проставлении более чем одного чекбокса начинается неразбериха.
Помогите пожалуйста любыми подсказками и ссылками, времени уже начало пятого утра, сил совсем нет, а очень надо(

Анонимный комментирует...

извиняюсь за обилие комментариев)
я сделал независимое проставление чекбоксов от раскрытия групп, но осталась проблема странного перескакивания галочек при раскрытии и закрытии групп(
и я не знаю в какую сторону думать чтобы теперь знать какие чекбоксы проставлены (сейчас я использую display_header) писать класс который будет запоминать состояние чекбокса?

Анонимный комментирует...

Спасибо, а то я английский не очень знаю и поэтому с оф документацией все плохо. Не мог разобраться с ExpandableListAdapter, теперь хоть все понятно :)

Кусака комментирует...

Подскажите, откуда взялось " ...android:mode="twoLine"...
"

в примере с SeekBar?

AntonDerevyanko комментирует...

Дарья, ваш блог сохранён у меня в закладках. По ряду вопросов быстрее "вспомнить/взять код-сэмпл" у вас здесь, чем на оверфлоу или девелоперсах. Ещё раз спасибо! Удачи вам! Пишите ещё, нам интересно.

П.С. у вас был пост о галереи... недавно вышла библиотека с удобной реализацией "прокрутки пальцем". Если интересно - пишите, м.б. вместе подготовим постик или просто дам ссылку.

nobody комментирует...

Я не понял одного момента. Вы пишите: private static class SeekBarBinder, а потом пишете: adapter.setViewBinder(new SeekBarBinder()). Как это? Вы создаете объект статического класса?