Нынешняя статья будет посвящена такой важной теме, как адаптеры. Ведь каждому, кому приходится сталкиваться с 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; } }
Пара слов о волшебных разметках
В приведенном примере в качестве идентификатора разметки передана загадочная константа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());
Отмечу, что логика работы
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<T>
, но биндить таким образом объекты произвольного класса оказывается бессмысленным, потому что получается примерно следующее:В виденных мной примерах
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; } }
А еще есть
SimpleCursorTreeAdapter
— абстрактный класс, унаследовавшись от которого и определив всего одну функцию, можно заполнить раскрывающийся список из курсоров. Я уже не буду приводить здесь пример, но в исходниках к статье он есть.Заключение
Итак, в статье мы дали определение адаптерам и расмотрели некоторые из стандартных адаптеров.Исходники примера
А что за девайс, если не секрет?
ОтветитьУдалитьПросто тоже подумываю купить себе чего-нибудь.
Думал про дешевый SE XPeria X10 Mini, но он уж очень дешёвый. Это-то и настараживает.
я себе на прошлой неделе Nexus One взял, в основном ради возможности халло ворд накодить, ибо к джаве теплые чувства ипытываю.
ОтветитьУдалитькстати интересно, это в последней версии SDK все еще нужны костыли по типу ViewBinder? как то несолидно... в свое время меня жутко расстроило кривое поведение GTK+ в мультипоточных приложениях (в исходниках LinuxDCPP таких кошмаров насмотрелся), а теперь такое и от гугля.... абидна (
2 AT:
ОтветитьУдалитьHTC Desire
Поражает количество кода для создания списков разных типов объектов, то бишь адаптеров создания списков. На самом деле объектов всего два - текст и изображение, тот же SeekBar - это всего лишь специализированное изображение, надо просто обновлять изображение в списке по требованию. Всё построено задом наперёд, не как сервисы, то есть список должен просить сервис (элемент списка) отрисоваться на своём Canvas, а элемент списка рисует на своём Canvas, а затем список копирует себе
ОтветитьУдалитьА что подразумевается под собственным адаптером?! Это пример, где расширяется SimpleAdapter?!
ОтветитьУдалитьТо что искал, человеческим языком!
ОтветитьУдалитьДарья Вы молодец! :)
Дашенька, Вы такая умница. Успехов Вам!
ОтветитьУдалитьЕсть ли возможность реализовать многоуровневую группировку?
ОтветитьУдалитьНапример: Группа->Подгруппа->Подподгруппа->елемент
Может кто подскажет как заставить обновиться ListView при изменении данных используя SimpleAdapter. C ArrayAdapter все хорошо, а с этим приходится каждый раз вызывать ListView.setAdapter()
ОтветитьУдалитьЗдравствуйте Дарья, вы не могли бы куда- нибудь залить файлы, связанные со стандартными разметками, просто по адресу "http://android.git.kernel.org/platform/frameworks/base.git/" отсутствует "~\core\res\res\layout"...
ОтветитьУдалитьАнонимный #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>, не подскажите что это за такой тип возвращаемый!
ОтветитьУдалить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?
Дарья, ваш блог сохранён у меня в закладках. По ряду вопросов быстрее "вспомнить/взять код-сэмпл" у вас здесь, чем на оверфлоу или девелоперсах. Ещё раз спасибо! Удачи вам! Пишите ещё, нам интересно.
ОтветитьУдалитьП.С. у вас был пост о галереи... недавно вышла библиотека с удобной реализацией "прокрутки пальцем". Если интересно - пишите, м.б. вместе подготовим постик или просто дам ссылку.
Я не понял одного момента. Вы пишите: private static class SeekBarBinder, а потом пишете: adapter.setViewBinder(new SeekBarBinder()). Как это? Вы создаете объект статического класса?
ОтветитьУдалить