среда, 16 января 2013 г.

Планирование дел: Кастомизация ListView. Постановка задачи

Планирование дел: Кастомизация ListView. Постановка задачи
С этой статьи начинается небольшой цикл статей о кастомизации списков (ListView). Мы напишим небольшое приложение по планированию своих дел. В первой части я затрону моменту по созданию ListView, его кастомизации, добавлению дополнительных элементов (Header, Footer) и заполнению списка.

Постановка задачи

По окончанию цикла статей наше приложение должно будет выполнять следующие функции:
  1. Добавление/Удаление/Выбор/Переименование запланированного дела.
  2. Удаление всех запланированных дел.
  3. Выбор/Отмена выбора всех запланированных дел.
  4. Сохранение/Загрузка запланированных дел на карте памяти.
  5. Анимация удаления запланированного дела.
  6. Анимация летающих ячеек.

Немного теории

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;

Сообщения используются для Handlera. Они дают возможность обновить лист не из главного потока.
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;
 }


Ссылки

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

  1. Хорошая статья, не так много статей по кастомизации списков. Может у Вас есть какие-то наработки в области изменения внешнего вида для ListFragment? Это какой-то ужас, но нигде нету нормального туториала, как в планшетной версии сделать для списка градиентный бэкграунд типа тени ,а так же выделение текущего элемента с помощью хотя бы треугольничка. В общем, как это сделано в Gmail. Информации вообще какие-то крохи ,а то, как выглядит по умолчанию - это ж убожество какое-то... Заранее спасибо!

    ОтветитьУдалить
    Ответы
    1. Спасибо! С ListFragment, к сожалению, пока не сталкивался. Но попробую разобраться и написать в одной из следующих статей =)

      Удалить