Sunday, February 8, 2009

Android TabActivity customization

Half of this weekend was spent in a desperate attempt to customize Android TabActivity.
According to developers community previous Android API releases allowed for tab look and feel modifications.

Unfortunately, mighty Google decided to revoke this ability and, trust me, they're pretty good at doing so.

It's obvious they've done it on purpose and I can't justify their decision. I can only fight back with whatever I have left.

I found a few guys trying to do similar stuff, so I decided to share:

  1. Create a selector drawable in your project. You can just copy paste the one from Android sources called tab_indicator.xml:
    <?xml version="1.0" encoding="utf-8"?>
    <!-- I drew these states myself, that's why they look so ugly. -->
    <selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Non focused states -->
    <item android:state_focused="false" android:state_selected="false" android:state_pressed="false" android:drawable="@drawable/tab_unselected" />
    <item android:state_focused="false" android:state_selected="true" android:state_pressed="false" android:drawable="@drawable/tab_selected" />

    <!-- Focused states -->
    <item android:state_focused="true" android:state_selected="false" android:state_pressed="false" android:drawable="@drawable/tab_focus" />
    <item android:state_focused="true" android:state_selected="true" android:state_pressed="false" android:drawable="@drawable/tab_focus" />

    <!-- Pressed -->
    <item android:state_pressed="true" android:drawable="@drawable/tab_press" />
    </selector>

  2. You've done item 1 and already hate me because compiler shows errors? This means you've done it right. Now create corresponding drawables for each selected/focused/pressed state. 9-patches are preferable. Errors should go away.
  3. In your TabActivity in onCreate() method after you've constructed all the tabs do the following:

    for (int i = 0; i < tw.getChildCount(); i++) {
    View v = tw.getChildAt(i);
    v.setBackgroundDrawable(getResources().getDrawable(R.drawable.tour_tab_indicator));
    }
  4. Depending on your new tab design you may be already happy or you may notice a thin line of a standard gray color under unselected tabs. It turns into orange upon touch. To fix it your have to use even hackier way. TabWidget contains 2 drawables that actually form those thin lines.

    private Drawable mBottomLeftStrip;
    private Drawable mBottomRightStrip;

    I haven't tried this yet especially on the actual device, but unless they've changed core JVM stuff that should be easy to change a private variable through reflection. Maybe I'll do it later and will post an update, but for now wish you a good luck with that part. :)
Make sure that R used is from your project.

What you've just done - you've replaced background selector drawable set by TabActivity with your own, which may essentially be the same, but uses your own 9-patches.

Aftermath:
This solution is rather a hack way around and relies on the current TabWidget implementation and as a result may not be compatible with future changes to TabWidget.
On the other hand I hope future changes to the TabActivity API will provide a better way for developers to alter tab look and feel.

2 comments:

  1. Your solution may work, but as you note, it is somewhat fragile, being dependent upon the current implementation.

    Taking advantage of the fact that Android is open source, you could fork the Tab* classes into your own namespace and alter them as you see fit. If you do that, not only do you have complete control, but you have the basis for submitting patches back to the project to have your enhancements be considered for the core product.

    This will give you a result that is more likely to be stable with respect to Android updates (since more of it is your code). Properly packaged, it could be a welcome addition to the Android community. Submitting patches to the Tab* classes would help the largest number of developers.

    "It's obvious they've done it on purpose and I can't justify their decision. I can only fight back with whatever I have left."

    However, my proposed solution will not give you the self-righteous sense of martyrdom you expressed in the above quote.

    ReplyDelete
  2. Thank you for your comments.

    I see your point and that was actually one of the alternatives, which I may explore later.

    The solution, however, is not ideal either. It's more than one class to be duplicated and their implementations depend on implementations of other standard classes that may change and invalidate the duplicated code. So the solution may arguably be just as fragile.

    Plus I'm going to lose all other updates and features added to the tab functionality by official Android team.

    ReplyDelete