Wednesday, January 16, 2013

Planning: Custom ListView. Formulation of the problem

Planning: Customization ListView. Formulation of the problem This article begins a small series of articles about customizing lists (ListView). We'll write a little app to plan your cases. In the first part I will cover moment to create a ListView, its customization, adding extra elements (Header, Footer) and fill the list.

Statement of the problem

At the end of our series of articles the application must perform the following functions:
  1. Add / Delete / Select / Move scheduled case.
  2. Delete all scheduled cases.
  3. Select / deselect all scheduled cases.
  4. Save / Load cases scheduled on the card.
  5. Animation delete scheduled case.
  6. Animation flying cells.

Some theory

ListView is a view group that displays a list of scrollable items. The list items are automatically inserted to the list using an Adapter that pulls content from a source such as an array or database query and converts each item result into a view that's placed into the list.
The basic algorithm development and customization ListView as follows:
1. First, you must develop a data structure which will represent one item. It can be a simple type or object (such as String or Integer), or developed class, as in our case (ToDoItem).
2. In the next step, it is necessary to develop all xml layout:
  • layout for a list item (listitem.xml);
  • layout for the Activity, which will be our ListView (in this case, main.xml);
  • layout for the Header and Footer, if they (list_header.xml, list_footer.xml).
3. Next, you need to develop adapter (CustomListAdapter.java) for the formation of our ListView to the specified data source. In this case, the adapter will be extended from the BaseAdapter and the data source will be used ArrayList.
4. The last step - the union of all developed in our Activity. Algorithm for object initialization next:
  •             load or initialize list (ArrayList) of the data; 
  • initialize ListView (using the findViewById()); 
  • create adapter with the downloaded data; 
  • if there is a Header or Footer, that add them to the ListView (using methods addHeaderView() or addFooterView(), respectively); 
  • set an adapter for ListView (using the setAdapter()).
For correct work of ListView, you should follow these rules:
  • Header and Footer to be added before calling setAdapter (); 
  • if you added Header or Footer, that note that the parameter position in the method onItemClick () for ListView will:
- For one more, if you have added Header;
- One or two larger than the data source (method size() for the ArrayList, if added 1 (or 2) View, respectively).

Practice

The images used in the project can be downloaded here.




Before the development layouts and classes, we must add the string resources.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">CustomListView</string>
       <string name="txt_header_title">ToDo List</string>
       <string name="txt_footer_title">Done: %s</string>
</resources>
1. Development of the project will begin with the development of the class ToDoItem. Objects of this class will be the data source for the items in the list.
Set the following attributes of the class:
name - the name of the case;
check - done / not done by the current case;
index - the position in the list when adding (to be used to sort the list).
The class will be serialized in order to be able to save and load.
Source code:
import java.io.Serializable;

/**
 * An item to the list
 */
public class ToDoItem implements Serializable{

 private static final long serialVersionUID = 2008719019880549886L;
 
 private String name; // Name of the case
 private boolean check; // Done / Not done
 private int index; // Position in the list when adding
 
 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. Development of layouts
To begin with develop xml-layout to represent a single element in the list.On the layout of a few comments:
  • android:focusable="false" used for the CheckBox, so that you can handle the press a list item (method onItemClick () for ListView).
Source code (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>
Now, designed layouts for the Header and Footer. Header will be used for the header and Footer will display the number completed cases.

Source code (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>

Source code (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>

Now develop main layout that will ListView. Standard ListView by clicking on the item highlighted in black. To avoid this, it is necessary to add a property: android:cacheColorHint="@android:color/transparent".

Source code (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. Development of adapter
In this example, we will use the simplest adapter extended from BaseAdapter. The source of data will be ArraList<ToDoItem>.

Source code (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;

/**
 * Adapter for 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);
  
  // Getting an item from list
  final ToDoItem item = list.get(position);
  
  // Setting the name of the case
  TextView listitem_name = (TextView)v.findViewById(R.id.listitem_name);
  listitem_name.setText(item.getIndex() + ". " + item.getName());  
  // Strikethrough completed cases
  if (item.isCheck())
   listitem_name.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.STRIKE_THRU_TEXT_FLAG);
  else 
   listitem_name.setPaintFlags(Paint.ANTI_ALIAS_FLAG);
  
  // Setting checkbox and events on click
  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. Development of a specialized class Utils.
The class I am developing an alone, because it uses methods (static), which we can use in the future in other classes.
Method () is used to remove the default selection for the item in the list and divider between the elements.
Method sorting() is used to sort the list. Ability to sort by done / not done cases (type = 0) and the index (type = 1).

Source code (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;

/**
 * Specialized class for various static methods
 */
public class Utils {

 /**
  * Set various parameters for the list. In particular, it removes the divider in a list.
  */
 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);
 }
 
 /**
  * Sort the list
  * @param type 0 - sort by checkmark
  * @param type 1 - sort by index
  */
 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. Main class MainActivity.
The attributes of our class will be the following:
// Messages for Handler
public static final int MSG_UPDATE_ADAPTER   = 0;
public static final int MSG_CHANGE_ITEM   = 1; 
 
// Objects for working with to-do list
private List<ToDoItem> list = new ArrayList<ToDoItem>();
private CustomListAdapter adapter;
private ListView listview;
 
// Footer for ListView 
private View footer;
Messages are used to Handler. They make it possible to update the list is not from the main thread.
Footer shall be made to the attributes that you can update it at runtime.
Now do all the necessary initialization in the method onCreate().
@Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
  
  initList(); // Initialization list
  
  listview = (ListView)findViewById(R.id.listview);
  
  /*
   Adding header and footer to the ListViewNote:
   1. These Views must be added before calling setAdapter
   2. These Views are taken into account in obtaining the position of an item in the list (for example in the way OnItemClickListener (for ListView) and getView (for 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(); // Calculation done cases
  listview.setAdapter(adapter);

  
  listview.setOnItemClickListener(new OnItemClickListener() {

   @Override
   public void onItemClick(AdapterView parent, View view, int position,
     long id) {  
    // If there is a tap on the Header or Footer, do not do anything
    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 set up and sort our list of the check box
  Utils.setList(listview, this);
  Utils.sorting(list, 0);

 }
Add methods to initialize the list and calculation done cases:
/**
  * Initialization list
  */
 private void initList()
 {
  for (int i = 1; i <= 10; i++)
   list.add(new ToDoItem("ToDo Item " + i, i));
 }
 
 /**
  * Calculate amount cases done
  */
 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));
 }
The final step will be the development Handler:
private Handler handler = new Handler() {
  public void handleMessage(Message msg) {

   switch (msg.what)
   {
   case MSG_UPDATE_ADAPTER: // ListView updating
    adapter.notifyDataSetChanged();
    setCountPurchaseProduct();
    break;
   case MSG_CHANGE_ITEM: // Do / not do case
    ToDoItem item = list.get(msg.arg1);
    item.setCheck(!item.isCheck());
    Utils.sorting(list, 0);
    adapter.notifyDataSetChanged();
    setCountPurchaseProduct();
    break; 
   }
  }
 };
 
 public Handler getHandler()
 {
  return handler;
 }

Links