Кастомизация ListView является обычным явлением при разработке Приложения под ОС Android. ListView (Список) позволяет отображать однотипную информацию в Вашем Приложении, например показывать товары с ценами и характеристиками, если вы разрабатываете Приложение для онлайн продаж. При этом являясь по сути рутинной операцией, тем не менее, некоторые вопросы доставляют начинающим разработчикам массу неудобств. Некоторые из этих неудобств и вопросов будут разобраны в данной статье.
Структура проекта
В данном уроке предлагается создать небольшой список с флагами стран, названием валют этих стран и аббревиатурой этих стран.
Всего для создания списка используется четыре страны: Россия, Южная Африка, Сингапур, Турция. Флаги этих стран находятся в папке drawable. Также присутствует xml файл для создания бэкграунда ListViewи флагов. Для хранения названий и аббревиатур используется файл strings.xml. Также используется отдельный xml файл для создания разметки элемента ListView.
В Проекте присутствует три класса:
- MainActivity
- MyListAdapter - который отвечает за передачу данных в форму элемента списка
- DataFlags - класс, который призван облегчить передачу данных в Адаптер ListView
Формирование разметки элемента ListView
Для создания приятного глазу списка в данном уроке использовались следующие файлы:
- list_item.xml
- flag.xml
- listview_selector.xml
- styles.xml
listview.xml
В данном файле приводится разметка непосредственно элемента listview.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="48dp" android:background="@drawable/listview_selector" android:orientation="horizontal"> <ImageView android:id="@+id/flagImage" android:src="@drawable/flag_russian" android:layout_width="48dp" android:layout_height="40dp" android:layout_marginStart="16dp" android:layout_marginLeft="16dp" android:layout_marginRight="8dp" android:layout_marginEnd="8dp" android:contentDescription="@string/flag" android:background="@drawable/flag" android:layout_gravity="center_vertical"/> <TextView android:id="@+id/abbreviation" android:layout_width="52dp" android:layout_height="30dp" android:textColor="@color/black" android:textSize="24sp" android:text="@string/russianRUB" android:layout_marginRight="6dp" android:layout_marginEnd="6dp" android:layout_gravity="center_vertical"/> <TextView android:id="@+id/name" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/black" android:textSize="14sp" android:text="@string/russian" android:gravity="center" android:layout_gravity="center" android:singleLine="false"/> </LinearLayout>
flag.xml
В данном файле создан backgroud для ImageView, в котором будет отображаться флаг. Края подложки для флага скруглены. А также настроен цвет подложки.
<?xml version="1.0" encoding="UTF-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <solid android:color="@color/blue_grey_100" /> <padding android:bottom="2dp" android:top="2dp" android:right="2dp" android:left="2dp"/> <corners android:radius="4dp"/> </shape>
listview_selector.xml
Данная разметка используется как background для элемента ListView. В данном файле настраивается цвет фона элемента в нормальном режиме и при нажатии на элемент списка.
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_selected="false" android:state_pressed="false" android:drawable="@color/white" /> <item android:state_pressed="true" android:drawable="@color/blue_grey_100" /> <item android:state_selected="true" android:state_pressed="false" android:drawable="@color/blue_grey_100" /> </selector>
styles.xml
Файл стилей знаком всем, кто занимается разработкой под Android. В данном случае он используется только лишь для хранения кодов цветов, используемых в данном примере.
<resources> <!-- Base application theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> </style> <color name="white">#ffffff</color> <color name="black">#000000</color> <color name="blue_grey_100">#CFD8DC</color> </resources>
activity_main.xml
Разметка основной активити, где будет располагаться наш кастомный ListView
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"> <ListView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/listViews" /> </RelativeLayout>
Основной класс проекта - MainActivity.java
В Данном классе производится запуск основной активити проекта, в которой будет отображаться кастомный ListView. Данные в ListView передаются с помощью списка ArrayList, в который помечаются объекты данных для каждого элемента. Группировка данных для элемента ListView производится с помощью отдельного класса, формирующего структуру данных, в которой будут храниться ID изображения флага, ID строки названия валюты страны, а также ID строки Аббревиатур валют стран.
По сути в этом классе производится стандартная настройка ListView с тем отличием, что используется не обычный List, а ArrayList, в который передается объект, а не единичная строка.
package ru.evileg.listview; import android.os.Bundle; import android.support.v7.app.ActionBarActivity; import android.view.Menu; import android.view.MenuItem; import android.widget.ListView; import java.util.ArrayList; public class MainActivity extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Находим ListView в разметке активити ListView listView = (ListView) this.findViewById(R.id.listViews); // Создаём ListView, в котором будут храниться данные для списка ArrayList<DataFlags> list = new ArrayList<>(); list.clear(); /* Наполняем List данным. В данном случае данные передаются Объектом, * в котором хранятся id, указывающие на флаги в ресурсах, а также * на Строки, в которых прописаны названия валют стран и аббревиатуры этих валют */ list.add(new DataFlags(R.drawable.flag_russian, R.string.russian, R.string.russianRUB)); list.add(new DataFlags(R.drawable.flag_africa, R.string.africa, R.string.africaZAR)); list.add(new DataFlags(R.drawable.flag_singapore, R.string.singapore, R.string.singaporeSGD)); list.add(new DataFlags(R.drawable.flag_turkish, R.string.turkish, R.string.turkishTRY)); /* Создаём адаптер, который будет передавать данные из List`а в * элемент ListView и подключаем его непосредственно к ListView`у */ MyListAdapter myListAdaptery = new MyListAdapter(this, list); listView.setAdapter(myListAdaptery); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } }
Все строки, которые применяются для ListView хранятся непосредственно в файле ресурсов strings.xml
<resources> <string name="app_name">ListView</string> <string name="action_settings">Settings</string> <string name="africaZAR">ZAR</string> <string name="russianRUB">RUB</string> <string name="singaporeSGD">SGD</string> <string name="turkishTRY">TRY</string> <string name="africa">South African\nrand</string> <string name="russian">Russian\nrouble</string> <string name="singapore">Singapore\ndollar</string> <string name="turkish">Turkish\nlira</string> <string name="flag">flag</string> </resources>
Класс DataFlags.java
Данный класс служит для формирования объекта, в котором хранятся данные для элементов ListView. Каждый объект обслуживает один из элементов ListView в данном проекте. Класс представляет собой общность переменных, и набор get методов для получения данных из объекта, формируемого данным классом.
package ru.evileg.listview; /** * Application ListView * Created by EvILeg on 21.07.2015. */ public class DataFlags { private int flagID; private int nameID; private int abbreviationID; public DataFlags (int _flagID, int _nameID, int _abbreviationID){ flagID = _flagID; nameID = _nameID; abbreviationID = _abbreviationID; } public int getFlagID(){ return flagID; } public int getNameID(){ return nameID; } public int getAbbreviationID() { return abbreviationID; } }
Класс MyListAdapter
Самый важный класс в данном уроке. Именно этот класс служит для передачи данных из ArrayList в ListView. Данный класс расширяет возможности родительского класса BaseAdapter.
package ru.evileg.listview; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; import java.util.ArrayList; /** * Application ListView * Created by EvILeg on 21.07.2015. */ public class MyListAdapter extends BaseAdapter { /* * Создаем объекты для отображения внешнего вида элемента * и объекта списка, с которым будет производиться работа */ private LayoutInflater LInflater; private ArrayList<DataFlags> list; /* * Конструктор класса. В данном случае лишь транслируется лист с данными * в лист адаптера, с которым будет производиться непосредственная работа */ public MyListAdapter(Context context, ArrayList<DataFlags> data){ list = data; LInflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); } /* * Далее идут стандартные методы родительского класса BaseAdapter. * Внимательно ознакомьтесь с отличиями методов в уроке и методов, * которые автоматически создает Android Studio. * Данные методы должны работать непосредственно с используемым нами ArrayList * и структурой данных, формируемой классом DataFlags */ @Override public int getCount() { return list.size(); } @Override public DataFlags getItem(int position) { return list.get(position); } @Override public long getItemId(int position) { return position; } /* * Метод, в котором формируется внешний вид элементов с его наполнением */ @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; View v = convertView; /* * В том случае, если вид элемента не создан, производится его создание * с помощью ViewHolder и тегирование данного элемента конкретным holder объектом */ if ( v == null){ holder = new ViewHolder(); v = LInflater.inflate(R.layout.list_item, parent, false); holder.flag = (ImageView) v.findViewById(R.id.flagImage); holder.name = (TextView) v.findViewById(R.id.name); holder.abbreviation = ((TextView) v.findViewById(R.id.abbreviation)); v.setTag(holder); } /* * После того, как все элементы определены, производится соотнесение * внешнего вида, данных и конкретной позиции в ListView. * После чего из ArrayList забираются данные для элемента ListView и * передаются во внешний вид элемента */ holder = (ViewHolder) v.getTag(); DataFlags dataFlags = getData(position); holder.flag.setImageResource(dataFlags.getFlagID()); holder.name.setText(v.getResources().getString(dataFlags.getNameID())); holder.abbreviation.setText(v.getResources().getString(dataFlags.getAbbreviationID())); return v; } /* * Метод, который забирает объект из ArrayList для дальнейшей работы с ним * и передачи его данных в элемент ListView */ DataFlags getData(int position){ return (getItem(position)); } /* * Данная структура данных необходима для того, чтобы при пролистывании * большого списка не возникало артефактов и перескакивания данных с одной позиции ListView * на другую, что достигается тегированием каждого элемента ListView */ private static class ViewHolder { private ImageView flag; private TextView name; private TextView abbreviation; } }
Использование ViewHolder принципиально важно, поскольку, если не применять данный прием, можно получить неадекватной поведение ListView, при котором будет происходит неконтролируемое перескакивание данных с одной позиции ListView на другую. Причем это будет происходит только внешне, а на самом деле данные будут оставаться на своих местах. Будет возникать несоответствие внешнего вида ListView с его содержанием.
Итог
Если в процессе изучения урока не возникло никаких проблем и ошибок, то внешний вид полученного ListView должен быть подобен следующему:
Кастомный ListView
Кастомный ListView с нажатым элементом
Спасибо за пример.
У меня возникает ошибка.
AndroidRuntime: FATAL EXCEPTION: main
Подскажите, пожалуйста, с чем это может быть связано?
Честно, вот в этой ошибке может быть что угодно, вплоть до того, что вы просто где-то не в том порядке код запсали.
Это просто ошибка в рантайме, для всего, что могло случиться.