Search This Blog

Loading...

Friday, October 29, 2010

Experience - Android Drag and Drop List

I am continually surprised by the fact that the things I need for my application do not exist in android. This time I needed a drag and drop list. I have been unsuccessful in finding a simple, working drag and drop list that I can extend. I have seen numerous places that reference android's music app as a starting place. The main classes of importance from the music app were TouchInterceptor and TrackBrowserActivity. Many of the examples that I have seen struggled to use it well and most ended up having serious problems as a result. I would rate the TouchInterceptor code with a rating of "F" because it is not simple and it is not easily extendable. Check out this article for a way to rate code. It left a good impression on me.

This experience is about my efforts to build a simplified drag and drop list. I hope time will prove that it is easy to extend as well. The first task was to get a copy of TouchInterceptor and TrackBrowserActivity running in a simplified project. This took time because of all the dependencies and interconnections between these two classes and the rest of the app. As I look back on the matter the only real benefit this provided me with was a better understanding of how not to make TouchInterceptor work. After I had a runnable project, I attempted to modify the drag animation from an expanding/contracting items on drag to one that rearranged items in place. I was almost finished with this change when I ran into a technical issue that I didn't want to spend time to fix. I then discovered that the Launcher app provided a better drag and drop example because it was simplified compared to some of the complex interactions in the music app. Using both the music app and the launcher app as a reference I wrote a simplified drag and drop project. You can find the project here.

I wanted to provide methods that allowed modifying/customizing the view of the item being dragged, but not fall into the same complex interactions of the music app. I chose to use the DragListener with the intended purpose of handling view changes. In the sample project, I made the item's view invisible to show where the drag view came from, and I changed the background color to show that a drag view is different from the rest of the item views. It is important to revert any changes to the item view because it will be recycled back into the list view for item views.

This project code is simply to share a simple working drag and drop list since I was unable to find one elsewhere on the web. If you add any extensions to this simple project I would like to hear about it in the comments.

Wednesday, October 13, 2010

Experience - Customizing Android's Tab Indicator

I was displeased by the initial look-n-feel of the Tabs on my TabActivity. When I ran the app on the emulator the non-selected tabs had a nice gray background and the selected tab was white. No issue here, but when I ran the exact same app on my device all the backgrounds were white as well as the text on each tab. This was a horrible combination it was nearly impossible to see any part of the Tabs. A description that I like of this problem can be found here.

Below I have listed several ways that I tried to fix the problem as well as the final solution that I decided on.

1) Customize theme & xml for Tab.
I started off looking for a way to customize the xml on TabWidget, TabHost, and even TabHost.Tabspec. I found out that TabHost didn't have much. TabWidget looked promising as I could change the look-n-feel of the divider and the bottom strip. Two nice features but what about the tab indicator? I then looked at TabHost.TabSpec this seemed to be the most promising but I found no xml attributes to change. Still wanting to change the look with xml I started digging into the theme looking for a way to change the look of a tab indicator. I was operating under the impression that this was just a theme problem and I thought a simple customization to the theme and all TabWidgets would display properly. The best resource I found can be found here, however this did not solve my problem.

2) Accessing TabWidget's children in code.
The second approach I took after a quick Google search was to try Advance Tab's idea. I was able to successfully change the tab indicators in code using a line similar to the following:

final TextView tv = (TextView) tabWidget.getChildAt(i).findViewById(android.R.id.title);

tv.setTextColor(this.getResources().getColorStateList(R.color.text_tab_indicator));


Changing the look through code is less than ideal and calling TabWidget's getChildAt() method was definitely not how the designers intended to change the tab indicator. I was very happy to have made some progress. However, I still wanted to be able to change the look in xml, so this approach didn't really solve my problem either.

3) Building a view for Tab's Indicator.
After several attempts and time spent on things that produced less than desirable results, I realized I had overlooked something simple. The "ah ha" moment (as I like to call it) came when I took another look at the TabHost.TabSpec's setIndicator() method. Since API level 4 we have been able to create a view and set it as the tab indicator. I felt like this way was simplistic and easy.

Below is a quick overview of how I configured my tab indicator view.
I created a tabindicator.xml like the following:

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:background="@drawable/tabindicatorselector"
android:layout_width="fill_parent" android:layout_height="fill_parent">

<TextView android:id="@+id/title"
android:layout_below="@id/title" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" android:text="Title">
</TextView>
<ImageView android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"/>
</RelativeLayout>


The code in my TabActivity looks like this:

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

View indicator1 = getLayoutInflater().inflate(R.layout.tabindicator, null);
TextView title1 = (TextView)indicator1.findViewById(R.id.title);
title1.setText(R.string.Tab1Title);

TabHost tabHost = getTabHost();
tabHost.addTab(tabHost.newTabSpec("tab1")
.setIndicator(indicator1)
.setContent(new Intent(this, TabGroup1Activity.class)));
}


I have left out some details, but hopefully this gives the basic idea on how I built my own tab indicator view. Good luck!

Wednesday, September 29, 2010

Experience - Multiple Android Activities in a TabActivity

The problem that I had was when I started a new activity it covered up the Tabs on my TabActivity. I could put a single Activity into a Tab, but adding a second Activity resulted in losing view of the Tabs. Then I learned about ActivityGroups which have a LocalActivityManager. I realized it was possible to have multiple Activities within an ActivityGroup after all this is what a TabHost does. I then realized if I set the content of each Tab to an ActivityGroup I could start multiple Activities in each Tab. I developed the below ActivityGroup.

import java.util.ArrayList;

import android.app.Activity;
import android.app.ActivityGroup;
import android.app.LocalActivityManager;

import android.content.Intent;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.Window;

/**
* The purpose of this Activity is to manage the activities in a tab.
* Note: Child Activities can handle Key Presses before they are seen here.
* @author Eric Harlow
*/
public class TabGroupActivity extends ActivityGroup {

private ArrayList<String> mIdList;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (mIdList == null) mIdList = new ArrayList<String>();
}

/**
* This is called when a child activity of this one calls its finish method.
* This implementation calls {@link LocalActivityManager#destroyActivity} on the child activity
* and starts the previous activity.
* If the last child activity just called finish(),this activity (the parent),
* calls finish to finish the entire group.
*/
@Override
public void finishFromChild(Activity child) {
LocalActivityManager manager = getLocalActivityManager();
int index = mIdList.size()-1;

if (index < 1) {
finish();
return;
}

manager.destroyActivity(mIdList.get(index), true);
mIdList.remove(index);
index--;
String lastId = mIdList.get(index);
Intent lastIntent = manager.getActivity(lastId).getIntent();
Window newWindow = manager.startActivity(lastId, lastIntent);
setContentView(newWindow.getDecorView());
}

/**
* Starts an Activity as a child Activity to this.
* @param Id Unique identifier of the activity to be started.
* @param intent The Intent describing the activity to be started.
* @throws android.content.ActivityNotFoundException.
*/
public void startChildActivity(String Id, Intent intent) {
Window window = getLocalActivityManager().startActivity(Id,intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
if (window != null) {
mIdList.add(Id);
setContentView(window.getDecorView());
}
}

/**
* The primary purpose is to prevent systems before android.os.Build.VERSION_CODES.ECLAIR
* from calling their default KeyEvent.KEYCODE_BACK during onKeyDown.
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
//preventing default implementation previous to android.os.Build.VERSION_CODES.ECLAIR
return true;
}
return super.onKeyDown(keyCode, event);
}

/**
* Overrides the default implementation for KeyEvent.KEYCODE_BACK
* so that all systems call onBackPressed().
*/
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
onBackPressed();
return true;
}
return super.onKeyUp(keyCode, event);
}

/**
* If a Child Activity handles KeyEvent.KEYCODE_BACK.
* Simply override and add this method.
*/
@Override
public void onBackPressed () {
int length = mIdList.size();
if ( length > 1) {
Activity current = getLocalActivityManager().getActivity(mIdList.get(length-1));
current.finish();
}
}
}


I got caught up trying to figure out if and how LocalActivityManager handled stepping back up the view hierarchy. After viewing the note on the LocalActivityManager.dispatchCreate(bundle state) which states "Note: This does not change the current running activity, or start whatever activity was previously running when the state was saved. That is up to the client to do, in whatever way it thinks is best."
I decided to add back tracking to the ActivityGroup. I attempted to make it behave the same as Activities outside an ActivityGroup by only needing to call finish() on an Activity and the previous Activity will be restarted or recreated.
I have also added the ability for the ActivityGroup to manage the back button being pressed. Android OS pre 2.0 handles things differently than the newer interface. A reference to this can be found here under Key Events Executed on Key-Up. I have tested the code on a device with OS 1.6 and the emulator with OS 2.1. Note: Child Activities like a ListActivity may handle the back button just override the appropriate methods for your OS version, and call onBackPressed() for the (parent) ActivityGroup. An example would look like this for OS 2.1.

@Override
public void onBackPressed() {
TabGroupActivity parentActivity = (TabGroupActivity)getParent();
parentActivity.onBackPressed();
}


The following section explains how I used this ActivityGroup. The first thing I did was subclass this ActivityGroup like this.

import android.content.Intent;
import android.os.Bundle;

public class TabGroup1Activity extends TabGroupActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
startChildActivity("OptionsActivity", new Intent(this,OptionsActivity.class));
}
}


I added this TabGroup1Activity into my TabActivity like this.

TabHost tabHost = getTabHost();

tabHost.addTab(tabHost.newTabSpec("tab1")
.setIndicator("MESSAGES")
.setContent(new Intent(this, TabGroup1Activity.class)));


Then any Activity that you want to start in the ActivityGroup can be done in a way similar to this.

Intent frequentMessages = new Intent(getParent(), FrequentMessageActivity.class);
TabGroupActivity parentActivity = (TabGroupActivity)getParent();
parentActivity.startChildActivity("FrequentMessageActivity", frequentMessages);


And that is how I added multiple Activities with a back tracking ability to a Tab of a TabActivity. Remember to always add Activities to your AndroidManifest.xml. A sample project for this article can be found here.

Monday, August 23, 2010

Experience - Configuring Android ListView

I have configured my ListView differently than the standard Android example. This article is what I did and how it was accomplished. The biggest difference between the android example and mine is I didn't want a full screen ListView. This impacted both the code and the visuals of the ListView. In the standard example it is suggested that a ListActivity is created. I on the other hand already have an Activity that defines a screen and is bound to a XML layout file. In the layout file I added a ListView element with the id MessageList. I then reference this ListView in my Activity and add the needed components to it. Below are excerpts from the Activity.

public class MessageActivity extends Activity {

private ListView mMessageList;
//Additional properties ...

public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.messagepage);

//Bind XML objects
mMessageList = (ListView)findViewById(R.id.MessageList);
//...

//Additional setup and configuation
//...

//MessageList is a dummy content string array
//located in res/values/strings.xml
String[] mMessages =  

getResources().getStringArray(R.array.MessageList);

//Simple way for example
mMessageList.setAdapter(new ArrayAdapter(this,
android.R.layout.simple_list_item_1, mMessages));

// mMessageList.setAdapter(
// new EfficientAdapter(this, android.R.layout.simple_list_item_1,
// android.R.id.text1, mMessages));
}
}

 

At this point I have a ListView defined on a page with other content. The ListView is connected to an ArrayAdapter which supplies it with dummy content through a string array in res/values/strings.xml. Next I wanted to add a border to the ListView to help define its size and make it clear where the edges of the ListView were. I was surprised that it was so difficult to figure out. There is no border property in ListView which was disappointing. I tried various options and found the solution I like the best is putting the ListView in a FrameLayout. I set the Background color of the FrameLayout to the color I want for my border and set the padding to the desired border width. The results look something like this.

Thursday, August 19, 2010

Experience - Android Orientation Change

I must admit I fell into a bit of a trap on this experience. As a result I will present failed attempts I had at detecting an orientation change and then give the solution I ended up with at the end of the article. My goal was to change the layout of some widgets when in portrait mode. I had designed the screen for landscape mode. Then when I rotated it on the emulator I was unsatisfied with the results. I had set up my layout so that only two LinearLayouts needed to have their orientation set from horizontal to verticle. I then set out to discover the best way to detect a change in orientation.

The first approach I tried was to use an android.view.OrientationEventListener. I used some slightly modified code from this article to test it out. I think that article is great, but I was less than satisfied with the results from OrientationEventListener.  First the emulator appeared to take longer when switching orientation. But the biggest problem was the onOrientationChanged() method was never called! Very disappointing! I assume I forgot to set something up properly, but it is hard without finding some proper documentation. I have also seen various blog articles stating problems with it since 2007 and it still doesn't seem to function as one would think here in 2010. :(

My second approach was to follow this form post. I thought this was a cleaver little trick that just might do the job for me. I got it to work by adding android:configChanges="orientation" to my AndroidManifest.xml file and

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
}

to my Activity. I had to add this method without the @Override compile and run and then add back the @Override. Eclipse was throwing an error at me. Within the newConfig parameter was an orientation property that I used for changing the layout logic. I really liked this option but the downside was onConfigurationChanged() was only being called when changing from landscape to portrait which is all I really needed but I didn't like that it only worked half the time it was suppose to.

The third approach I looked at was from this article. I read the article which I liked and saw the comments about SensorManager's API has been changed three times, and could use at least one more update in my opinion. I thought that I would be able to make it work and was able to until I tried to register the SensorEventListener with the SensorManager. At this point I considered it another failure and maybe a lost cause.

After these three failed attempts I changed my thinking. I had been trying to find a clean and efficient way of detecting an orientation change and changing a couple properties on my layouts. A new thought that I had was what if I just create an entirely new layout to be shown in portrait mode.

These are the steps I used.
I opened up my xml layout that I wanted to change. At the top were some options that look like this.




I changed the config drop down to portrait, and then hit the create button on the right hand side.
The only option I selected was portrait.



and with that I had a new xml that I was editing and setting up for portrait mode. When I run the app in the emulator it chooses the right layout for portrait and the other layout for landscape.
Done deal.
P.S.
    Which turns out to be a good thing because the portrait layout is going to be arranged completely different now. :)

Tuesday, August 17, 2010

Top facts about the name Eric Harlow

Top facts about the name Eric Harlow
I ran into these doing a quick google search.
Found it interesting so I thought I would post it.

Wednesday, August 11, 2010

Develop software the way you mow the lawn

My girlfriend picked up a lawnmower for free the other day. She had not had a lawn mower for the majority of the summer and had only been able to mow her lawn a couple times. The grass was sparse with areas of tall weeds. She was short on time, so she took her new mower and mowed a couple of circles in key locations where the tall weeds were growing. This got me to thinking. Isn't this what so many developers do when starting a project or even throughout a project? They write a little code here and there, and they attack one issue just enough to tame it some. They then jump to another spot to do a little work there. They jump from one thing to the next very quickly without completing any single task. This is common for new developers and I find it easy for myself to fall into this pattern. The red flag that went up in my head was there is no way that my girlfriend will be able to mow all the grass. She will miss places, and there will be grass left untouched. An observer walking down the street can quickly spot the missed patches and will think what a horrible mow job. The same goes for a software project. If starting a new software project or working on an existing project be careful not to fall into this type of pattern. That method of programming will intrinsically leave areas in the application lacking or buggy. The next time you are starting a project take a moment to remember how you would mow your grass. It may help your project to be more successful.