Постановка задачи
По окончанию цикла статей наше приложение должно будет
выполнять следующие функции:
- Добавление/Удаление/Выбор/Переименование запланированного дела.
- Удаление всех запланированных дел.
- Выбор/Отмена выбора всех запланированных дел.
- Сохранение/Загрузка запланированных дел на карте памяти.
- Анимация удаления запланированного дела.
- Анимация летающих ячеек.
Немного теории
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, к сожалению, пока не сталкивался. Но попробую разобраться и написать в одной из следующих статей =)
Удалить