Tuesday, June 16, 2009

StateListDrawable and TransitionDrawable

StateListDrawable and TransitionDrawable are useful classes when it comes to making custom buttons and controls.

Making a custom button is simple. First of all you need to create some graphics for the different states of your button (see the Android dev guide on basic 2d Graphics).

I created a couple NinePatchDrawable image files using GIMP and the draw9patch.bat tool included in the Android SDK under <sdk-dir>/tools/. Here they are:

bluebutton.9.png


redbutton.9.png


Android already has its own default built-in buttons which are pretty similar to these. Of course, in a real app you can give the buttons different shapes and designs. Also, since these are NinePatch images, I could have made them smaller to save space since they'd stretch the same way anyway.

To use these in my project, I created a couple XML files under my project's res/drawable directory, pressbutton.xml and longpressbutton.xml.

res/drawable/pressbutton.xml:


<selector android="http://schemas.android.com/apk/res/android">
<item state_pressed="true" drawable="@drawable/redbutton">
<item drawable="@drawable/bluebutton">
</selector>



With selectors (StateListDrawables), the object displays the first <item> underneath the <selector> tag whose attributes match the state of the object. Since the last <item> in my example doesn't have any attributes like state_pressed, it acts as a catch-all.




res/drawable/longpressbutton.xml:


<transition android="http://schemas.android.com/apk/res/android">
<item drawable="@drawable/redbutton">
<item drawable="@drawable/bluebutton">
</transition>



The <transition> (TransitionDrawable) is more of an animation definition, so you don't need attributes on the <item>s. It plays through the <item>s sequentially based on the argument you pass into the startTransition(milliseconds) method.

Now, getting TransitionDrawables to work is a bit trickier than using StateListDrawables. Well, at least it took me longer to figure out.

I made a subclass of Button called TransitionButton, like this:



public class TransitionButton extends Button {
private TransitionDrawable mTransition = null;
private Context mContext;

public TransitionButton(Context context) {
super(context);
mContext = context;
}
public TransitionButton(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
}
public TransitionButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mContext = context;
}

public void setTransition(TransitionDrawable transition) {
mTransition = transition;
setBackgroundDrawable(transition);
}

public void setPressed(boolean pressed) {
super.setPressed(pressed);
if (pressed && mTransition != null) {
mTransition.startTransition(1000);
} else if (!pressed && mTransition != null) {
mTransition.resetTransition();
}
}
}


To use the Button and TransitionButton, as usual I put the necessary code into the onCreate(Bundle) method of my Activity. So I have something like this:




public class StateAndTransition extends Activity {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

final Context mContext = getApplicationContext();
Resources res = mContext.getResources();
TransitionDrawable transition = (TransitionDrawable) res.getDrawable(R.drawable.longpressbutton);

final Button stateButton = (Button) findViewById(R.id.statebutton);
stateButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
stateButton.setTextSize(10 + stateButton.getTextSize());
}
});


final TransitionButton longpressButton = (TransitionButton) findViewById(R.id.longpress);
longpressButton.setTransition(transition);

longpressButton.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent m) {
if (m.getAction() == MotionEvent.ACTION_DOWN) {
longpressButton.setPressed(true);
} else {
longpressButton.setPressed(false);
}
return true; // The Listener consumes the Touch action
}
});

}


}




Finally, my res/layout/main.xml:



<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/talklittle.android.examples.stateandtransition"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>

<Button android:id="@+id/statebutton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerInParent="true"
android:text="Touch me"
android:textSize="8sp"
android:background="@drawable/pressbutton"/>

<talklittle.android.examples.stateandtransition.TransitionButton
android:id="@+id/longpress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerInParent="true"
android:text="Hold me"
android:textSize="30sp"
android:background="@drawable/redbutton"/>

</LinearLayout>




Now, that will hopefully compile and run (with minor changes like adding the imports and package statements). If so, then you should see that the Button using a StateListDrawable works fine, while the TransitionButton is buggy. It looks like this at the end of the transition:

No, we don't want it to look like that. What's going on? The answer is that since we're using NinePatch images, the padding area of the images is not changed when we use a TransitionDrawable for the background. We can fix this by either using a regular Drawable (xxx.png instead of xxx.9.png) or by drawing padding lines that extend all the way along the right and bottom edges, making button text ugly unless you manually size the Buttons.

With custom Buttons though, I think most of the time you'd be using static sized images rather than NinePatch images, but that's just my opinion. I was just too lazy this time to make more than two images.

Anyway that's about it for this small tutorial. Hope it makes sense! I wrote it mostly for myself to document this procedure, since it took awhile to figure out the details.

Sunday, June 14, 2009

Wish my wireless router was better

I was able to get my Android Dev phone set up very easily without a data plan, just using a wireless connection and Linux, by following the directions on this very useful post from technomancy.org. Luckily I had an Ubuntu VM ready to go, so the entire setup process took no more than ten minutes.

Unfortunately, though, my wireless router is a bit old and sucky, and the wireless craps out from time to time. On top of that, the phone won't keep retrying the connection; I believe it tries to reconnect to your remembered wifi access points once each, then after that you have to manually go back and press the button to reconnect. (I'm running Android software 1.1. I don't think it's fixed yet in cupcake 1.5.) It's quite annoying. Maybe I'll have to put down the money for a data plan....

I think the folks in the issue tracker for #2718 are having the same issue on Cupcake though. Oh well, at least I'm not addicted to using the web from my phone yet. Mostly because "Crackdroid" sounds stupid.


On an unrelated note, I wonder how many android blogs on Blogger use this "Son of Moto" template? sigh... I'll try to come up with something better.