Постановка задачи
По окончанию цикла статей наше приложение должно будет
выполнять следующие функции:
- Добавление/Удаление/Выбор/Переименование запланированного дела.
- Удаление всех запланированных дел.
- Выбор/Отмена выбора всех запланированных дел.
- Сохранение/Загрузка запланированных дел на карте памяти.
- Анимация удаления запланированного дела.
- Анимация летающих ячеек.
Немного теории
ListView – это специальное View, которое отображает список прокручиваемых элементов. Эти
элементы списка автоматически формируются с помощью адаптера, который берет
содержимое из различных источников, например массив или база данных, и
преобразует каждый элемент во View,
которое помещается в лист.
Основной алгоритм разработки и кастомизации ListView следующий:
1. Сначала необходимо
разработать структуру данных, которая будет представлять один элемент списка.
Это может быть простой тип или объект (такой как String или Integer), либо разработанный
класс, как в нашем случае (ToDoItem).
2. На следующем шаге, необходимо разработать все xml макеты:
- макет для одного элемента списка (listitem.xml);
- макет для Activity, где будет находиться наш ListView (в нашем случае это main.xml);
- макет для Header и Footer, если они будут (list_header.xml, list_footer.xml).
3. Далее необходимо разработать Адаптер (CustomListAdapter.java) для формирования
нашего ListView по указанному источнику данных. В нашем случае адаптер будет
наследоваться от BaseAdapter
и в качестве источника данных будет использоваться ArrayList.
4. Последний шаг – это объединение всего выше разработанного
в нашем Activity. Алгоритм
инициализации объектов следующий:
- загружаем или инициализируем список (ArrayList) с данными;
- инициализируем ListView (с помощью метода findViewById());
- создаем адаптер с загруженными данными;
- если есть Header или Footer, то добавляем их к ListView (с помощью методов addHeaderView() или addFooterView() соответственно);
- устанавливаем адаптер для ListView (с помощью метода setAdapter()).
Для корректной работы ListView, необходимо придерживаться следующих правил:
- Header и Footer необходимо добавлять до вызова метода setAdapter();
- если Вы добавили Header или Footer, то заметьте, что параметр position в методе onItemClick() для ListView будет:
- на +1
больше, если добавлен Header;
- на 1 (2)
больше размера источника данных (метод size() для ArrayList,
если добавлено 1 (или 2) View
соответственно).
Практика
Заметка: Для того чтобы комментарии в скаченном проекте были
читаемы (русский язык), необходимо поменять кодировку проекта на UTF-8 (Project –> Properties –> Resource –> Text file incoding).
Изображения, используемые в проекте, можно скачать здесь.
Перед разработкой макетов и классов, надо добавить строковые ресурсы.
Перед разработкой макетов и классов, надо добавить строковые ресурсы.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">CustomListView</string>
<string name="txt_header_title">Надо сделать</string>
<string name="txt_footer_title">Сделано: %s</string>
</resources>
1. Разработку проекта начнем с разработки класса ToDoItem. Объекты этого
класса будут являться источником данных для элементов в списке.
Аттрибуты класса зададим следующие:
name - название дела;
check - сделано/не сделано текущее дело;
index - позиция в списке в момент
добавления (будет использоваться для сортировки списка).
Класс будет сериализован для того, чтобы можно было его
сохранить и загрузить.
Исходный код класса:
import java.io.Serializable; /** * Айтем для списка */ public class ToDoItem implements Serializable{ private static final long serialVersionUID = 2008719019880549886L; private String name; // Название дела private boolean check; // Сделано/Не сделано private int index; // Позиция в списке в момент добавления public String getName() { return name; } public void setName(String name) { this.name = name; } public boolean isCheck() { return check; } public void setCheck(boolean check) { this.check = check; } public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } public ToDoItem(String name, int index) { setName(name); setIndex(index); setCheck(false); } public ToDoItem() { setName(""); setCheck(false); } }
2. Разработка макетов
Для начала разработаем xml-макет для представления отдельного элемента в массиве.
По данному макету есть несколько замечаний:
- android:focusable="false" используется для CheckBox, чтобы можно было обработать нажатие на элемент в списке (метод onItemClick() для ListView).
Исходный код listitem.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:background="@drawable/item"
android:orientation="horizontal" >
<TextView
android:id="@+id/listitem_name"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="left|center_vertical"
android:layout_weight="1"
android:singleLine="true"
android:ellipsize="end"
android:layout_marginLeft="10dp"/>
<CheckBox
android:id="@+id/listitem_check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="5dp"
android:button="@drawable/custom_checkbox"
android:focusable="false"/>
</LinearLayout>
</LinearLayout>
Теперь разработаем макеты для Header и Footer. Header будет
использоваться для заголовка, а Footer будет выводить количество завершенных дел.
Исходный код list_header.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:background="@drawable/header" >
<TextView
android:id="@+id/header_title"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:shadowColor="@android:color/black"
android:shadowDy="1"
android:shadowRadius="2"
android:textColor="@android:color/white"
android:textSize="20sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
Исходный код list_footer.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:background="@drawable/footer" >
<TextView
android:id="@+id/footer_title"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginRight="2dp"
android:gravity="right|center_vertical"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
Теперь разработаем основной макет, в котором будет ListView. Стандартный ListView при нажатии на элемент или сам лист, выделяется черным цветом. Чтобы этого избежать, надо добавить свойство: android:cacheColorHint="@android:color/transparent".
Исходный код main.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white" >
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:cacheColorHint="@android:color/transparent" />
</LinearLayout>
3. Разработка адаптера
В данном примере будет использоваться самый простой адаптер,
наследованный от BaseAdapter.
В качестве источника данных будет ArraList<ToDoItem>.
Исходный код класса CustomListAdapter:
import java.util.ArrayList; import java.util.List; import android.content.Context; import android.graphics.Paint; import android.os.Message; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.CheckBox; import android.widget.TextView; /** * Адаптер для ListView */ public class CustomListAdapter extends BaseAdapter { private LayoutInflater inflater_; private List<ToDoItem> list = new ArrayList<ToDoItem>(); private Context context; public CustomListAdapter(List<ToDoItem> list, Context context) { this.list = list; inflater_ = LayoutInflater.from(context); this.context = context; } @Override public int getCount() { return this.list.size(); } @Override public Object getItem(int position) { return this.list.get(position); } @Override public long getItemId(int position) { return position; } public void setList(List<ToDoItem> list) { this.list = list; } @Override public View getView(final int position, View view, ViewGroup parent) { View v = view; if (view == null) v = inflater_.inflate(R.layout.listitem, parent, false); v.setVisibility(View.VISIBLE); // Получение айтема из листа final ToDoItem item = list.get(position); // Установка названия дела TextView listitem_name = (TextView)v.findViewById(R.id.listitem_name); listitem_name.setText(item.getIndex() + ". " + item.getName()); // Перечеркивание завершенных дел if (item.isCheck()) listitem_name.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.STRIKE_THRU_TEXT_FLAG); else listitem_name.setPaintFlags(Paint.ANTI_ALIAS_FLAG); // Установка чекбокса и события на нажатие CheckBox listitem_check = (CheckBox)v.findViewById(R.id.listitem_check); listitem_check.setChecked(item.isCheck()); listitem_check.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Message msg = new Message(); msg.arg1 = position; msg.what = MainActivity.MSG_CHANGE_ITEM; ((MainActivity)context).getHandler().sendMessage(msg); } }); return v; } }
4. Разработка специализированного класса Utils.
Этот класс я выношу отдельно, посколько в нем используются
методы (статические), которые мы можем использовать в дальнейшем в других
классах.
Метод setList()
используется для того, чтобы убрать стандартное выделение элемента в списке и
разделительную полосу между элементами.
Метод sorting() используется для сортировки списка. Возможна
сортировка по сделанным/не сделанным делам (type = 0) и по индексу (type = 1).
Исходный код класса Utils:
import java.util.Collections; import java.util.Comparator; import java.util.List; import android.content.Context; import android.graphics.drawable.ColorDrawable; import android.widget.ListView; /** * Специализированный класс для различных статических методов */ public class Utils { /** * Устанавливает различные параметры для листа. В частности, убирает полосу разделения * у элементов списка. */ public static void setList(ListView list, Context context) { list.setSelector(android.R.color.transparent); ColorDrawable sage = new ColorDrawable(context.getResources().getColor( android.R.color.transparent)); list.setDivider(sage); list.setDividerHeight(0); } /** * Сортировка списка * @param type 0 - сортировка по галочке * @param type 1 - сортировка по индексу */ public static void sorting(List<ToDoItem> list, final int type) { Collections.sort(list, new Comparator<ToDoItem>() { @Override public int compare(ToDoItem item1, ToDoItem item2) { int compare = 0; switch (type) { case 0: Boolean bool_value1 = Boolean.valueOf(item1.isCheck()); Boolean bool_value2 = Boolean.valueOf(item2.isCheck()); compare = bool_value1.compareTo(bool_value2); if (compare == 0) compare = (item1.getIndex() > item2.getIndex()) ? 1 : -1; break; case 1: Integer int_value1 = Integer.valueOf(item1.getIndex()); Integer int_value2 = Integer.valueOf(item2.getIndex()); compare = int_value1.compareTo(int_value2); break; } return compare; } }); } }
5. Основной класс MainActivity.
Атрибуты нашего класса будут следующие:
// Сообщения для Handler'а public static final int MSG_UPDATE_ADAPTER = 0; public static final int MSG_CHANGE_ITEM = 1; // Объекты для работы со списком дел private List<ToDoItem> list = new ArrayList<ToDoItem>(); private CustomListAdapter adapter; private ListView listview; // Footer нашего ListView private View footer;
Сообщения используются для Handler’a. Они дают возможность обновить лист не из главного потока.
Footer выносится в атрибуты, чтобы можно было его обновлять в
процессе выполнения программы.
Теперь сделаем всю необходимую инициализацию в методе onCreate().
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); initList(); // Инициализация списка listview = (ListView)findViewById(R.id.listview); /* Добавление header'a и footer'а к ListView Заметьте: 1. Эти View необходимо добавлять до вызова метода setAdapter 2. Эти View учитываются при получении позиции айтема в списке (например в методах OnItemClickListener (для ListView) и getView (для Adapter)) */ View header = LayoutInflater.from(this).inflate(R.layout.list_header, null, false); ((TextView)header.findViewById(R.id.header_title)).setText(R.string.txt_header_title); listview.addHeaderView(header); footer = LayoutInflater.from(this).inflate(R.layout.list_footer, null, false); listview.addFooterView(footer); adapter = new CustomListAdapter(list, this); setCountPurchaseProduct(); // Расчет сделанных дел listview.setAdapter(adapter); listview.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { // Если произошло нажатие по Header или Footer, то ничего не делаем if (position == 0 || position == list.size() + 1) return; Message msg = new Message(); msg.arg1 = position - 1; msg.what = MSG_CHANGE_ITEM; handler.sendMessage(msg); } }); // Настраиваем ListView и сортируем наш список по галочкам Utils.setList(listview, this); Utils.sorting(list, 0); }
Добавим методы для инициализации списка и расчета сделанных дел:
/** * Инициализация списка */ private void initList() { for (int i = 1; i <= 10; i++) list.add(new ToDoItem("ToDo Item " + i, i)); } /** * Рассчитать количество сделанных дел */ private void setCountPurchaseProduct() { int count = 0; Iterator<ToDoItem> it = list.iterator(); while (it.hasNext()) { ToDoItem item = it.next(); if (item.isCheck()) count++; } ((TextView)footer.findViewById(R.id.footer_title)).setText(String.format(getString(R.string.txt_footer_title), count)); }
Последним шагом останется разработка Handler’a:
private Handler handler = new Handler() { public void handleMessage(Message msg) { switch (msg.what) { case MSG_UPDATE_ADAPTER: // Обновление ListView adapter.notifyDataSetChanged(); setCountPurchaseProduct(); break; case MSG_CHANGE_ITEM: // Сделать/Не сделать дело ToDoItem item = list.get(msg.arg1); item.setCheck(!item.isCheck()); Utils.sorting(list, 0); adapter.notifyDataSetChanged(); setCountPurchaseProduct(); break; } } }; public Handler getHandler() { return handler; }
Хорошая статья, не так много статей по кастомизации списков. Может у Вас есть какие-то наработки в области изменения внешнего вида для ListFragment? Это какой-то ужас, но нигде нету нормального туториала, как в планшетной версии сделать для списка градиентный бэкграунд типа тени ,а так же выделение текущего элемента с помощью хотя бы треугольничка. В общем, как это сделано в Gmail. Информации вообще какие-то крохи ,а то, как выглядит по умолчанию - это ж убожество какое-то... Заранее спасибо!
ОтветитьУдалитьСпасибо! С ListFragment, к сожалению, пока не сталкивался. Но попробую разобраться и написать в одной из следующих статей =)
Удалить