Thursday 21 April 2011

Custom Spinner with Text and Icons

Spinner is a widget similar to Drop-Down list for selecting items.

Step 1: Start a new Project named CustomSpinner

 Step 2: Open the res/layout/main.xml and insert the following

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >
<Spinner
    android:id="@+id/spinner"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" />
</LinearLayout>

Step 3: Create row.xml in res/layout and insert the following :

 <?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"
 android:orientation="horizontal">
<ImageView
 android:id="@+id/icon"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:src="@drawable/icon"
 android:layout_marginLeft="15dip" />
<TextView
 android:id="@+id/weekofday"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_marginLeft="5dip"
 android:layout_marginTop="10dip"
 android:text="Sunday"
 android:textColor="#000000"/>
</LinearLayout>


Step 4: Now open CustomSpinner.java and add the following code :

package com.spinner.demo;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.TextView;

public class CustomSpinner extends Activity {
   
    String[] DayOfWeek = {"Sunday", "Monday", "Tuesday",
            "Wednesday", "Thursday", "Friday", "Saturday"};
   
    Integer[] image = {
                 R.drawable.change,
                R.drawable.change1,
                R.drawable.change2,
                R.drawable.change3,
                R.drawable.change4,
                R.drawable.change5,
                R.drawable.change1,
                R.drawable.change2 };
   
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main1);
       
        Spinner mySpinner = (Spinner)findViewById(R.id.spinner);
        mySpinner.setAdapter(new MyCustomAdapter(CustomSpinner.this, R.layout.test, DayOfWeek,image));
    }
   
    public class MyCustomAdapter extends ArrayAdapter<String>{

        public MyCustomAdapter(Context context, int textViewResourceId,
                String[] objects, Integer[] image) {
            super(context, textViewResourceId, objects);
            // TODO Auto-generated constructor stub
        }

        @Override
        public View getDropDownView(int position, View convertView,
                ViewGroup parent) {
            // TODO Auto-generated method stub
            return getCustomView(position, convertView, parent);
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            // TODO Auto-generated method stub
            return getCustomView(position, convertView, parent);
        }

        public View getCustomView(int position, View convertView, ViewGroup parent) {
            // TODO Auto-generated method stub
            //return super.getView(position, convertView, parent);

            LayoutInflater inflater=getLayoutInflater();
            View row=inflater.inflate(R.layout.test, parent, false);
            TextView label=(TextView)row.findViewById(R.id.weekofday);
            label.setText(DayOfWeek[position]);
           
            ImageView icon=(ImageView)row.findViewById(R.id.icon);
            icon.setImageResource(image[position]);
            return row;
        }   
    }
}

Monday 4 April 2011

CursorAdapter with Alphabet-Indexed Section Headers

Hey everyone,I would like to share how to organize a CursorAdapter into sections alphabetically. It works as long as your queries in the Cursor are alphabetically sorted (so the sortOrder parameter of your query is ASC by the column you want to alphabetize by).


This is made easier by the AlphabetIndexer widget, which uses binary search to finds the position of the first word of each starting letter in your data. However, there are still a few subtleties that must be addressed.


There are two methods inherited from BaseAdapter that we need to consider:
-    getCount(): the number of items the ListView should display. Since now we wish to include one extra header in the list, for every alphabetical section, this should return num_items_in_data + num_sections
-    getItem(positition): returns the item in the data set associated with the position in the ListView. Note that there is an offset depending on how many section headers appear in the data. So in the picture above, to get Fig in the list with headers, since 4 headers appear (A,B,C,F), we want the data item with position 6, instead of the list index which has position 10.

And there are two methods that we implement for SectionIndexer:
-    getPositionForSection(section): returns the position of the beginning of each section (which in our case is alphabetical). As mentioned before, the AlphabetIndexer will give us the position of the first word starting with each letter in our data set (so it will tell us where A cooresponds to 0, B cooresponds to 1, F to 6, etc). We must offset these positions by the number of other headers that have appeared, so the new position of A is 0 + 0, new position of B is 1 + 1, new position of F is 6 + 3.
-    getSectionForPosition(position): returns which section each position belongs to. The AlphabetIndexer does this by linearly comparing the first letter of each word to each possible letter in the alphabet, but in our case we now have an offset to consider. I choose to not use the AlphabetIndexer at all for this, but do a similar thing.

Sample AlphabeticIndexer :

    public class DemoActivity extends ListActivity {
        private SQLiteDatabase db;
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);   
            final DbHelper helper = new DbHelper(this);
            db = helper.getWritableDatabase();
            final int result = helper.insertDummyData(db);
            if (result > 0){
                final Cursor cursor = db.query(DbHelper.TABLE_COUNTRIES, null, null,null, null, null,
                DbHelper.COUNTRIES_NAME + " ASC" );
                startManagingCursor(cursor);
                Toast.makeText(this, "Finished populating.", Toast.LENGTH_SHORT).show();
                setListAdapter(new MyAlphabetizedAdapter(this, android.R.layout.simple_list_item_1,
                cursor, new String[]{DbHelper.COUNTRIES_NAME}, new int[]{android.R.id.text1}));
                getListView().setFastScrollEnabled(true);
            } else {
                Toast.makeText(this, "Database could not be populated. Restart the activity.",
                Toast.LENGTH_LONG).show();
            }
        }
        @Override
        protected void onDestroy() {
            db.close();
            super.onDestroy();
        }

/**
* CursorAdapter that uses an AlphabetIndexer widget to keep track of the section indicies.
* These are the positions where we want to show a section header showing the respective alphabet letter.
*
*/
    public class MyAlphabetizedAdapter extends SimpleCursorAdapter implements SectionIndexer{
        private static final int TYPE_HEADER = 1;
        private static final int TYPE_NORMAL = 0;
        private static final int TYPE_COUNT = 2;
        private AlphabetIndexer indexer;
        private int[] usedSectionNumbers;
        private Map sectionToOffset;
        private Map sectionToPosition;
        public MyAlphabetizedAdapter(Context context, int layout, Cursor c,String[] from, int[] to) {
            super(context, layout, c, from, to);
            indexer = new AlphabetIndexer(c, c.getColumnIndexOrThrow(DbHelper.COUNTRIES_NAME),
            "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
            sectionToPosition = new TreeMap();
            sectionToOffset = new HashMap();
            final int count = super.getCount();
            int i;
            for (i = count - 1 ; i >= 0; i--){
                sectionToPosition.put(indexer.getSectionForPosition(i), i);
            }
            i = 0;
            usedSectionNumbers = new int[sectionToPosition.keySet().size()];
            for (Integer section : sectionToPosition.keySet()){
                sectionToOffset.put(section, i);
                usedSectionNumbers[i] = section;
                i++;
            }
            for(Integer section: sectionToPosition.keySet()){
                sectionToPosition.put(section, sectionToPosition.get(section) + sectionToOffset.get(section));
            }
        }

        @Override
        public int getCount() {
            if (super.getCount() != 0){
                return super.getCount() + usedSectionNumbers.length;
            }
            return 0;
        }

        @Override
        public Object getItem(int position) {
            if (getItemViewType(position) == TYPE_NORMAL){
                return super.getItem(position - sectionToOffset.get(getSectionForPosition(position)) - 1);
            }
            return null;
        }

        @Override
        public int getPositionForSection(int section) {
            if (! sectionToOffset.containsKey(section)){
                int i = 0;
                int maxLength = usedSectionNumbers.length;
                while (i < maxLength && section > usedSectionNumbers[i]){
                i++;
            }
            if (i == maxLength) return getCount(); //the given section is past all our data
                return indexer.getPositionForSection(usedSectionNumbers[i]) +  
                sectionToOffset.get(usedSectionNumbers[i]);
            }
            return indexer.getPositionForSection(section) + sectionToOffset.get(section);
        }

        @Override
        public int getSectionForPosition(int position) {
            int i = 0;
            int maxLength = usedSectionNumbers.length;
            while (i < maxLength && position >= sectionToPosition.get(usedSectionNumbers[i])){
                i++;
            }
            return usedSectionNumbers[i-1];
        }

        @Override
        public Object[] getSections() {
            return indexer.getSections();
        }

        @Override
        public int getItemViewType(int position) {
            if (position == getPositionForSection(getSectionForPosition(position))){
                return TYPE_HEADER;
            }
            return TYPE_NORMAL;
        }

        @Override
        public int getViewTypeCount() {
            return TYPE_COUNT;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            final int type = getItemViewType(position);
            if (type == TYPE_HEADER){
                if (convertView == null){
                    convertView = getLayoutInflater().inflate(R.layout.header, parent, false);
                }
                ((TextView)convertView.findViewById(R.id.header)).setText((String)getSections()
               [getSectionForPosition(position)]);
                return convertView;
            }
            return super.getView(position - sectionToOffset.get(getSectionForPosition(position)) - 1,
            convertView, parent);
            }

            @Override
            public boolean areAllItemsEnabled() {   
                return false;
            }

            @Override
            public boolean isEnabled(int position) {
                if (getItemViewType(position) == TYPE_HEADER){
                    return false;
                }
                return true;
            }
        }
    }

A database helper to create the db table with country names
public class DbHelper extends SQLiteOpenHelper {
  public static final String TABLE_COUNTRIES = "countries";
  public static final String COUNTRIES_NAME = "name";
  private static final String DATABASE_NAME = "alphabetical_tutorial.db";
  private static final int DATABASE_VERSION = 1;
  public DbHelper(Context context) {
   super(context, DATABASE_NAME, null, DATABASE_VERSION);
  }

  @Override
  public void onCreate(SQLiteDatabase db) {
   db.execSQL("create table " + TABLE_COUNTRIES + " (" +
   BaseColumns._ID + " integer primary key autoincrement,"
   + COUNTRIES_NAME + " text not null,"
   + "unique (" + COUNTRIES_NAME + ") on conflict replace)");
  }

  @Override
  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
   db.execSQL("drop table exists" + TABLE_COUNTRIES);
   onCreate(db);
  }

  /**
  * Inserts the list of country names into the db.
  * We use SQL transactions for data integrity and efficiency.
  * @param db
  * @return
  */
  public int insertDummyData(SQLiteDatabase db){
   int numInserted = 0;
   db.beginTransaction();
   try {
    SQLiteStatement insert = db.compileStatement("insert into " +
    TABLE_COUNTRIES + "(" + COUNTRIES_NAME + ")"
    + "values " + "(?)");
    for (String country : COUNTRIES){
     insert.bindString(1, country);
     insert.execute();
    }
    db.setTransactionSuccessful();
    numInserted = COUNTRIES.length;
   } finally {
    db.endTransaction();
   }
   return numInserted;
  }

   static final String[] COUNTRIES = new String[] { 
  "Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra",
 "Angola", "Anguilla", "Antarctica", "Antigua and Barbuda", "Argentina",
 "Armenia", "Aruba", "Australia", "Austria", "Azerbaijan","Brunei",
 "Bulgaria", "Burkina Faso", "Burundi","Cote d'Ivoire", "Cambodia",
 "Cameroon", "Canada", "Cape Verde","Cayman Islands","Chad",
 "Chile", "China","Christmas Island", "Cocos (Keeling) Islands",
 "Colombia", "Comoros", "Congo","Cook Islands", "Costa Rica",
 "Croatia", "Cuba", "Cyprus", "Czech Republic","Denmark",
 "Djibouti","Dominica","Dominican Republic","East Timor", "Ecuador",
 "Egypt", "El Salvador", "Equatorial Guinea","Eritrea","Estonia",
 "Ethiopia", "Faeroe Islands", "Falkland Islands", "Fiji","Finland",
 "Former Yugoslav Republic of Macedonia", "France", "French Guiana",
 "French Polynesia","French Southern Territories", "Gabon", "Georgia",
 "Germany", "Ghana", "Gibraltar","Greece", "Greenland", "Grenada",
 "Guadeloupe", "Guam", "Guatemala", "Guinea", "Guinea-Bissau","Guyana",
 "Haiti", "Heard Island and McDonald Islands", "Honduras", "Hong Kong",
 "Hungary","Iceland", "India", "Indonesia", "Iran", "Iraq", "Ireland",
 "Israel", "Italy", "Jamaica","Japan", "Jordan", "Kazakhstan", "Kenya",
 "Kiribati", "Kuwait", "Kyrgyzstan", "Laos","Latvia", "Lebanon", "Lesotho",
 "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg",
 "Macau", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali",
 "Malta", "Marshall Islands","Martinique", "Mauritania", "Mauritius",
 "Mayotte", "Mexico", "Micronesia", "Moldova","Monaco", "Mongolia",
 "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia","Nauru",
 "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", 
 "New Zealand","Nicaragua", "Niger", "Nigeria", "Niue", "Norfolk Island",
 "North Korea", "Northern Marianas","Norway", "Oman", "Pakistan", 
 "Palau", "Panama", "Papua New Guinea", "Paraguay", "Peru","Philippines", 
 "Pitcairn Islands", "Poland", "Portugal", "Puerto Rico", "Qatar",
 "Reunion", "Romania", "Russia", "Rwanda", "Sqo Tome and Principe", 
 "Saint Helena","Saint Kitts and Nevis", "Saint Lucia","Samoa", 
 "San Marino", "Saudi Arabia", "Senegal","The Gambia", "Togo",
 "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey",
 "Ukraine", "United Arab Emirates", "United Kingdom","United States",
 "United States Minor Outlying Islands", "Uruguay", "Uzbekistan",
 "Yemen", "Yugoslavia", "Zambia", "Zimbabwe"
    };
 } 
 
Now we need a layout for the header. A simple TextView will suffice: 
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
 android:id="@+id/header"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:padding="6dp"
    android:textSize="18sp"
    android:textStyle="bold"
    android:gravity="center"
    android:background="@android:color/white"
    android:textColor="@android:color/black"
    />