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.

Wednesday, November 3, 2010

Stackoverflow.com

I'd like to write in my first post about the best Project for programmers, where I found much help and answers for almost all my questions according programming for Android.

Meet 


This project is much better than any forum-place and even than official android developer group.

Hope, yoo'll find help there as well.