Friday, June 3, 2011

Using AutocompleteTextView with SQLite and CursorAdapter

I needed to make an AutocompleteTextView in an app using data from the database.

There is also an example on Android Documentation how to use AutoCompleteTextView. It's working fine until the number of entries is small. But if you have many of them (as it was in my case) everything starts working slower and slower.

I also needed to have my own filtering.

So I was looking for a solution for these problems. And there is one, its name is CursorAdapter.

The idea is simple: you don't need to store all results of your query, you just have a pointer on your data - the cursor to access data must be shown right now.

Our simple layout autocomplete.xml

<?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">
    <AutoCompleteTextView
        android:id="@+id/filter"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>

I created a small activity class that just shows AutocompleteTextView and doing nothing else. I use as data an existing database table from an app with some stations in it.

public class AutoCompleteActivity extends Activity
{
    private StationDBAdapter mDBAdabter;  // database adapter / helper
    private StationAdapter mCursorAdapter;  // cursor adapter
    private Cursor mItemCursor;    // and the cursor itself
   
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.autocomplete);
       
        initCursorAdapter();
        initItemFilter();       
    }
   
    // initialize the cursor adapter
 private void initCursorAdapter()
    {
        mDBAdabter = new StationDBAdapter(this);
       
        mItemCursor = mDBAdabter.getStationCursor("");       
        startManagingCursor(mItemCursor);
       
        mCursorAdapter = new StationAdapter(getApplicationContext(), mItemCursor);
               
    }
   
    // initialize AutocompleteTextView
 private void initItemFilter()
    {
        AutoCompleteTextView item_filter = (AutoCompleteTextView) findViewById(R.id.filter);
        item_filter.setAdapter(mCursorAdapter);
        item_filter.setThreshold(1);
    }   
}

As you see, I'm using in the activity class above the cursor adapter. And here is my implementation.
You must implement constructor, two abstract method of parent class CursorAdapter, override implementation of runQueryOnBackgroundThread.

public class StationAdapter extends CursorAdapter
{
    private StationDBAdapter dbAdapter = null;

    public StationAdapter(Context context, Cursor c)
    {
        super(context, c);
        dbAdapter = new StationDBAdapter(context);
        dbAdapter.open();
    }
   
    @Override
    public void bindView(View view, Context context, Cursor cursor)
    {
        String item = createItem(cursor);       
        ((TextView) view).setText(item);       
    }
   
    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent)
    {
        final LayoutInflater inflater = LayoutInflater.from(context);
        final TextView view = (TextView) inflater.inflate(R.layout.list_item, parent, false);
       
        String item = createItem(cursor);
        view.setText(item);
        return view;
    }

    @Override
    public Cursor runQueryOnBackgroundThread(CharSequence constraint)
    {
        Cursor currentCursor = null;
       
        if (getFilterQueryProvider() != null)
        {
            return getFilterQueryProvider().runQuery(constraint);
        }
       
        String args = "";
       
        if (constraint != null)
        {
            args = constraint.toString();       
        }

        currentCursor = dbAdapter.getStationCursor(args);

        return currentCursor;
    }
   
    private String createItem(Cursor cursor)
    {
        String item = cursor.getString(1);       
        return item;
    }
   
    public void close()
    {
        dbAdapter.close();
    }
}

I also want to give you a code fragement from a Database-helper-class with the query method to show an important detail: you have to have a field with identiefier _id in your query results, otherwise you will get an exception from the AbstractCursor-class.

ERROR/AndroidRuntime(1455): Caused by: java.lang.IllegalArgumentException: column '_id' does not exist
ERROR/AndroidRuntime(1455): at android.database.AbstractCursor.getColumnIndexOrThrow(AbstractCursor.java:314)
...


...
public Cursor getStationCursor(String args)
{       
 String sqlQuery = "";
 Cursor result = null;
   
 sqlQuery  = " SELECT _id" + ", stationName ";
 sqlQuery += " FROM stations";
 sqlQuery += " WHERE stationName LIKE '%" + args + "%' ";
 sqlQuery += " ORDER BY stationName";
   
 if (mDB == null)
 {
  open();
 }

 if (mDB!=null)
 {           
  result = mDB.rawQuery(sqlQuery, null);
 }
 return result;
}

...

Hope, it can help you by creating your own AutocompleteTextView

Friday, January 28, 2011

Problem with textColor by using selector

Recently I became the problem using selector as background image and textColor.

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="wrap_content" android:layout_width="fill_parent"
    android:gravity="center" android:focusable="true"
    android:minHeight="?android:attr/listPreferredItemHeight"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:background="@android:drawable/list_selector_background"/>

Without setting textColor value the text color wasn't that I wanted to have (WOW, surprise, surprise!!!).

And by setting textColor as a color value selector image didn't work anymore.

I couldn't explain my problem, so I've asked by StackOverflow, this time without success after several days.

But it's nice to have such friends like Google (as Germans say, Google is your Friend - Google ist dein Freund). And Google has found the answer for me.

Guess where? Yes, on StackOverflow :)

And here is the solution. Everything works fine if you'll set as text color another selector (in my case selector_icon_text_color.xml):
<?xml version="1.0" encoding="utf-8"?>
<selector
 xmlns:android="http://schemas.android.com/apk/res/android">
 <item
  android:state_pressed="false"
  android:color="#777777" />
 <item
  android:state_pressed="true"
  android:color="#AAAAAA" />
</selector>

And so modificated code for my TextView looks like:
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="wrap_content" android:layout_width="fill_parent"
    android:gravity="center" android:focusable="true"
    android:minHeight="?android:attr/listPreferredItemHeight"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:textColor="@drawable/selector_icon_text_color"
    android:background="@android:drawable/list_selector_background"/>

Thank you Google, thank you Stackoverflow.

Monday, November 8, 2010

Section Index for Android

Recently I'd asked, if it's possible to have iPhone's Section Index in Android.
I thougt it wouldn't be a problem and said yes. But it's wasn't so simple. I just hope, I haven't reinvented the wheel. I'll call it SideIndex.

Section Index for Android

This is xml-layout (main.xml) - a list view and a linear layout for side index within another linear layout:
<?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="wrap_content">
 <LinearLayout 
  android:orientation="horizontal"
  android:layout_width="fill_parent" 
  android:layout_height="wrap_content">
  <ListView 
   android:id="@+id/ListView01" 
   android:layout_width="0dp"
   android:layout_height="wrap_content" 
   android:layout_weight="1">

  </ListView>
  <LinearLayout 
   android:orientation="vertical"
   android:background="#FFF" 
   android:id="@+id/sideIndex"
   android:layout_width="40dip" 
   android:layout_height="fill_parent"
   android:gravity="center_horizontal">
  </LinearLayout>  
 </LinearLayout>
</LinearLayout>
In the activity class I define some member variables and an array with values for the ListView. This array isn't sorted.
public class SideIndex extends Activity
{ 
  private GestureDetector mGestureDetector;
   
  // x and y coordinates within our side index
  private static float sideIndexX;
  private static float sideIndexY;
   
  // height of side index
  private int sideIndexHeight;
   
  // number of items in the side index
  private int indexListSize;
   
  // list with items for side index
  private ArrayList<Object[]> indexList = null;
   
  // an array with countries to display in the list
  static String[] COUNTRIES = new String[]
  {
    "East Timor", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea",
    "Estonia", "Ethiopia", "Faeroe Islands", "Falkland Islands", "Fiji", "Finland",
    "Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra",
    "Angola", "Anguilla", "Antarctica", "Antigua and Barbuda", "Argentina",
    "Armenia", "Aruba", "Australia", "Austria", "Azerbaijan",
    "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium",
    "Monaco", "Mongolia", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia",
    "Nauru", "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand",
    "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",
    "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",
    "French Southern Territories", "Gabon", "Georgia", "Germany", "Ghana", "Gibraltar",
    "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guinea", "Guinea-Bissau",
    "Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia", "Moldova",
    "Bosnia and Herzegovina", "Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory",
    "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Saudi Arabia", "Senegal",
    "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands",
    "Somalia", "South Africa", "South Georgia and the South Sandwich Islands", "South Korea",
    "Spain", "Sri Lanka", "Sudan", "Suriname", "Svalbard and Jan Mayen", "Swaziland", "Sweden",
    "Switzerland", "Syria", "Taiwan", "Tajikistan", "Tanzania", "Thailand", "The Bahamas",
    "The Gambia", "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey",
    "Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates",
    "United Kingdom", "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan",
    "Vanuatu", "Vatican City", "Venezuela", "Vietnam", "Virgin Islands", "Wallis and Futuna",
    "Western Sahara", "British Virgin Islands", "Brunei", "Bulgaria", "Burkina Faso", "Burundi",
    "Cote d'Ivoire", "Cambodia", "Cameroon", "Canada", "Cape Verde",
    "Cayman Islands", "Central African Republic", "Chad", "Chile", "China",
    "Reunion", "Romania", "Russia", "Rwanda", "Sqo Tome and Principe", "Saint Helena",
    "Saint Kitts and Nevis", "Saint Lucia", "Saint Pierre and Miquelon",
    "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia",
    "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo",
    "Cook Islands", "Costa Rica", "Croatia", "Cuba", "Cyprus", "Czech Republic",
    "Democratic Republic of the Congo", "Denmark", "Djibouti", "Dominica", "Dominican Republic",
    "Former Yugoslav Republic of Macedonia", "France", "French Guiana", "French Polynesia",
    "Macau", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands",
    "Yemen", "Yugoslavia", "Zambia", "Zimbabwe"};
  // ...
}
Some initial methods:
@Override
public void onCreate(Bundle savedInstanceState)
{
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  // don't forget to sort our array (in case it's not sorted)
  Arrays.sort(COUNTRIES);

  final ListView lv1 = (ListView) findViewById(R.id.ListView01);
  lv1.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, COUNTRIES));
  mGestureDetector = new GestureDetector(this, new SideIndexGestureListener());
}

@Override
public boolean onTouchEvent(MotionEvent event)
{
  if (mGestureDetector.onTouchEvent(event))
  {
    return true;
  } else
  {
    return false;
  }
}
The we'll create for given array a list with items (first letters). For every letter we'll store the letter itself, the start and end position of items for this letter.

private ArrayList<Object[]> createIndex(String[] strArr)
{
  ArrayList<Object[]> tmpIndexList = new ArrayList<Object[]>();
  Object[] tmpIndexItem = null;

  int tmpPos = 0;
  String tmpLetter = "";
  String currentLetter = null;
  String strItem = null;

  for (int j = 0; j < strArr.length; j++)
  {
    strItem = strArr[j];
    currentLetter = strItem.substring(0, 1);

    // every time new letters comes
    // save it to index list
    if (!currentLetter.equals(tmpLetter))
    {
      tmpIndexItem = new Object[3];
      tmpIndexItem[0] = tmpLetter;
      tmpIndexItem[1] = tmpPos - 1;
      tmpIndexItem[2] = j - 1;

      tmpLetter = currentLetter;
      tmpPos = j + 1;

      tmpIndexList.add(tmpIndexItem);
    }
  }

  // save also last letter
  tmpIndexItem = new Object[3];
  tmpIndexItem[0] = tmpLetter;
  tmpIndexItem[1] = tmpPos - 1;
  tmpIndexItem[2] = strArr.length - 1;
  tmpIndexList.add(tmpIndexItem);

  // and remove first temporary empty entry
  if (tmpIndexList != null && tmpIndexList.size() > 0)
  {
    tmpIndexList.remove(0);
  }

  return tmpIndexList;
}
You can imagine, that not all item from side index could be displayed. If they are too much, only every m-th will be shown. I said, that the font size of every item should be minimum 20. Hence we could compute maximal number of items for this font size in the list. The problem is, you should know the height of the side index. If you'll try to get it in onCreate-methode, you'll get 0 back. Solution for it is to call getHeight in onWindowFocusChanged:
private ArrayList<Object[]> createIndex(String[] strArr)
{
  ArrayList<Object[]> tmpIndexList = new ArrayList<Object[]>();
  Object[] tmpIndexItem = null;

  int tmpPos = 0;
  String tmpLetter = "";
  String currentLetter = null;
  String strItem = null;

  for (int j = 0; j < strArr.length; j++)
  {
    strItem = strArr[j];
    currentLetter = strItem.substring(0, 1);

    // every time new letters comes
    // save it to index list
    if (!currentLetter.equals(tmpLetter))
    {
      tmpIndexItem = new Object[3];
      tmpIndexItem[0] = tmpLetter;
      tmpIndexItem[1] = tmpPos - 1;
      tmpIndexItem[2] = j - 1;

      tmpLetter = currentLetter;
      tmpPos = j + 1;

      tmpIndexList.add(tmpIndexItem);
    }
  }

  // save also last letter
  tmpIndexItem = new Object[3];
  tmpIndexItem[0] = tmpLetter;
  tmpIndexItem[1] = tmpPos - 1;
  tmpIndexItem[2] = strArr.length - 1;
  tmpIndexList.add(tmpIndexItem);

  // and remove first temporary empty entry
  if (tmpIndexList != null && tmpIndexList.size() > 0)
  {
    tmpIndexList.remove(0);
  }

  return tmpIndexList;
}

Another problem was to implement posibility to scroll within side index. Touch events for linear layout aren't enough for that. So we have to implement a SimpleOnGestureListener:
private ArrayList<Object[]> createIndex(String[] strArr)
{
  ArrayList<Object[]> tmpIndexList = new ArrayList<Object[]>();
  Object[] tmpIndexItem = null;

  int tmpPos = 0;
  String tmpLetter = "";
  String currentLetter = null;
  String strItem = null;

  for (int j = 0; j < strArr.length; j++)
  {
    strItem = strArr[j];
    currentLetter = strItem.substring(0, 1);

    // every time new letters comes
    // save it to index list
    if (!currentLetter.equals(tmpLetter))
    {
      tmpIndexItem = new Object[3];
      tmpIndexItem[0] = tmpLetter;
      tmpIndexItem[1] = tmpPos - 1;
      tmpIndexItem[2] = j - 1;

      tmpLetter = currentLetter;
      tmpPos = j + 1;

      tmpIndexList.add(tmpIndexItem);
    }
  }

  // save also last letter
  tmpIndexItem = new Object[3];
  tmpIndexItem[0] = tmpLetter;
  tmpIndexItem[1] = tmpPos - 1;
  tmpIndexItem[2] = strArr.length - 1;
  tmpIndexList.add(tmpIndexItem);

  // and remove first temporary empty entry
  if (tmpIndexList != null && tmpIndexList.size() > 0)
  {
    tmpIndexList.remove(0);
  }

  return tmpIndexList;
}
It's another important part of implementation. We compute here for every position a right item in the country list. Side index items are uniformly distributed. But there exist different number of countries for every letter. This should be kept in mind.

I must say, I can not really explain, what have I done. I've done it intuitive. And it works.
public void displayListItem()
{
  // compute number of pixels for every side index item
  double pixelPerIndexItem = (double) sideIndexHeight / indexListSize;

  // compute the item index for given event position belongs to
  int itemPosition = (int) (sideIndexY / pixelPerIndexItem);

  // compute minimal position for the item in the list
  int minPosition = (int) (itemPosition * pixelPerIndexItem);

  // get the item (we can do it since we know item index)
  Object[] indexItem = indexList.get(itemPosition);

  // and compute the proper item in the country list
  int indexMin = Integer.parseInt(indexItem[1].toString());
  int indexMax = Integer.parseInt(indexItem[2].toString());
  int indexDelta = Math.max(1, indexMax - indexMin);

  double pixelPerSubitem = pixelPerIndexItem / indexDelta;
  int subitemPosition = (int) (indexMin + (sideIndexY - minPosition) / pixelPerSubitem);

  ListView listView = (ListView) findViewById(R.id.ListView01);
  listView.setSelection(subitemPosition);
}
So, it works. I hope, it will work you as well.

I'll be thankfull for every tip of enhancement.

UPD:
Because of problem with SyntaxHiglighter I've added my project sources. You can download them here

Saturday, November 6, 2010

Статические переменные в Android приложениях

Недавно обнаружил проблему статических переменных в одном приложении под Android. Они просто через какое-то время пропадали.

Как обычно спросил у товарищей на StackOverflow, как бороться с данной проблемой и получил замечательное решение.

Вместо декларации статических переменных в какой-либо Активности (Actiivity) можно сделать это в специально классе, наследующим свойства класс Application, который является синглтоном и уничтожается в самую последнюю очередь.

Например, в файле strings.xml определена версия нашего приложения.

2.0.0

Не хотелось создавать ещё где-нибудь в коде константу версию. Помогло в этой задаче следущее решение:
public class MyApp extends Application
{
private static String appVersion = "";
public static void setAppVersion (String version)
{
appVersion = version;
}
public static String getAppVersion ()
{
return appVersion;
}
}

В самой первой активности происходит инитализация:
public void onCreate(Bundle savedInstanceState)
{
...
String appVersion = this.getString(R.string.version_name);
MyApp.setAppVersion(appVersion);
...
}

И далее с любого места в коде можно получить доступ к версии, даже в обычных Java классах:
...
String version = MyApp.getEasyGOVersion();
...

Это решение мне очень помогло. Надеюсь, поможет и вам.

Friday, November 5, 2010

Static variables in your Android Application

I've got recently problems in an android application with static variables, which were declared in some activities. After some time they seemed to be erased.

So I asked again people on StackOverflow and I've got a good solution for my problem.

Instead of declaring static variables in an activity you can also do it in your own subclass of the Activity class, which is a singleton.

Within strings.xml I have the application version.


2.0.0

I didn't want to store this value second time as a constant in my java code. And here is the solution.

public class MyApp extends Application
{
private static String appVersion = "";
public static void setAppVersion (String version)
{
appVersion = version;
}

public static String getAppVersion ()
{
return appVersion;
}
}

In my first activity I initialize version variables:
public void onCreate(Bundle savedInstanceState) 
{
...
String appVersion = this.getString(R.string.version_name);
MyApp.setAppVersion(appVersion);
...
}

Now version value can be read everywhere in your application also in normal java classes:

...
String version = MyApp.getEasyGOVersion();
...

It works fine for me and I hope, it will work for you as well.