Samstag, 15. Oktober 2011

How to tame the Android TabHost

If someone has experience in Android development, there is a rather high probability that she/he knows something about using tabs in Android. There are some helpful tutorials at the official Android developers site (even the usage with Fragments is explained), so it's not very difficult to start with.
http://developer.android.com/resources/tutorials/views/hello-tabwidget.html
http://developer.android.com/resources/samples/ApiDemos/src/com/example/android/apis/app/FragmentTabs.html

But there might come a certain point where using the TabHost becomes cumbersome, at least when it comes to the point of customizing the TabWidget used by the TabHost (to represent the actual tabs). Sure, there are some great tutorials out there showing how to completely customize the TabWidget, but nevertheless it proves rather difficult to completely accomplish ones needs. It's also possible to completely abandon the use of the TabHost and try another approach, even though there might be good reasons to use the TabHost. Just why is the TabHost so stiff?
Well there is a satisfiable solution for this problem: Just abandon the TabWidget and just use the TabHost "manually". In this case "abandon" means to set its visibility to "gone" and "manually" means handling tab transitions by code, i.e. using the appropriate methods given by the TabHost.

The results are:

  1. Complete freedom in creating your very own tab navigation
  2. Complete freedom in using tab management to add/remove tabs as you like and hide it from the user
  3. Using the advantages of the TabHost without even using a tabbed ui approach
"Advantages" of using the TabHost isn't applying for all kind of apps, but if you are certain that it's very appropriate for the content of your app to be processed by a tab like approach, these arguments might facilitate your decision to use the TabHost:
  • Spending less time for carrying about the life cycle of your activities
  • Very clear navigation structure,; you can centralize all transitions of your activities into a single component (e.g. your central TabActivity)
  • Instantiation of your activities is always done automatically by demand, i.e. by switching to the tab; no need to write any code for this at all
  • All stuff is basically done within a single activity (your TabActivity), it's very clear what's going on within the app
There are certainly more pros and also some cons. One of it is the fact, that your app takes more space in memory and your layout tree can grow larger because you always have two more FrameLayouts (the TabHost, which is a FrameLayout, and the @android:id/tabcontent element, which is also often a FrameLayout) between your app content and the main window as without the tab approach. So it could be necessary to spend some time thinking about disposing resources to keep performance high.

Now it's time to have a quick look at how to accomplish the idea of a more flexible TabHost.

The layout resource looks very simple:
<?xml version="1.0" encoding="utf-8"?>
<TabHost
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/tabhost"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TabWidget
        android:id="@android:id/tabs"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:visibility="gone"/>
    <FrameLayout
        android:id="@android:id/tabcontent"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"/>
</TabHost>
 As mentioned before the TabWidget is set to "gone", so it won't bother anymore.

The ui for the tabs can be done just as you wish. For this example I chose a navigation by the build in menu button. But there is also the possibility to do it by radiobuttons or a list in the home screen or dialog appearing on a button press or a gesture detector or etc. just anything.

So let's look inside our main TabActivity, which will provide the menu and the necessary tab switch mechanics.
public class Main extends TabActivity {

    // indices for each tab
    public static final int HOME_TAB = 0;
    public static final int NEWS_TAB = 1;
    // ... etc. ...
    // helpful indicators for which tab was selected
    public static int SELECTED_NEWS_TAB = NEWS_TAB;
    public static int SELECTED_BLOGS_TAB = BLOGS_TAB;

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

    /**
     * Delegate tab creation and adding.
     */
    private void setTabs() {
        // add the necessary tabs
        addTab(R.string.tag_home, HomeActivity.class);
        addTab(R.string.tag_news, NewsActivity.class);
        // ... etc. ...
    }

    /**
     * Create a tab as an Activity and add it to the {@link TabHost}
     * 
     * @param tagId
     *            resource id of the tag for finding the tab
     * @param activity
     *            the activity to be added
     */
    private void addTab(int tagId, Class<?> activity) {
        // create an Intent to launch an Activity for the tab (to be reused)
        Intent intent = new Intent().setClass(this, activity);
        // initialize a TabSpec for each tab and add it to the TabHost
        TabHost.TabSpec spec = getTabHost().newTabSpec(getString(tagId));
        spec.setContent(intent);
        spec.setIndicator(getString(tagId));
        getTabHost().addTab(spec);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.main_menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        super.onOptionsItemSelected(item);
        // find which menu item has been selected
        switch (item.getItemId()) {
        // check for each known menu item
        case (R.id.menu_home):
            getTabHost().setCurrentTab(HOME_TAB);
            break;
        case (R.id.menu_news):
            // check which tab to be selected
            if (SELECTED_NEWS_TAB == SINGLENEWS_TAB) {
                getTabHost().setCurrentTab(SINGLENEWS_TAB);
            } else {
                getTabHost().setCurrentTab(NEWS_TAB);
                SELECTED_NEWS_TAB = NEWS_TAB;
            }
            break;
        // ... tab switching ...
        }
        return true;
    }
}
That's all the code for creating and switching to the tabs. Why use Activities as tabs instead of Views? There are some benefits:

  • Use separate menus for each Activity; this won't be as easy when using Views (check for which view is actually visible)
  • When using Fragments, it's easier to manage them in separate Activities instead of one huge TabActivity
That's why I prefer to use an Activities for each tab.

Now a quick look into the menu layout:
<menu
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:name="Menu">
    <item
        android:id="@+id/menu_home"
        android:title="@string/menu_home"
        android:icon="@drawable/tab_home">
    </item>
    <item
        android:id="@+id/menu_news"
        android:title="@string/menu_news"
        android:icon="@drawable/tab_news">
    </item>
    <!-- ... etc. ... -->
</menu>
Each menu item provides an icon and a text description for each tab.

At last the basic structure of the Activities used for this example:
public abstract class MyAbstractActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContent();
    }

    protected void setContent() {
        setContentView(R.layout.content);
        ((TextView) findViewById(R.id.content)).setText(getContent());
    }

    protected abstract String getContent();

    @Override
    public void onBackPressed() {
        AlertDialog.Builder dialog = new AlertDialog.Builder(this).setMessage("Close app?").setCancelable(false)
                .setPositiveButton("YES", new OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        finish();
                    }
                }).setNegativeButton("No", new OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.cancel();
                    }
                });
        dialog.create().show();
    }
}
To have less boilerplate code, having a basic Activity that does setup and closing logic makes sense for this example.

A very rudimentary example of a concrete Activity:
Layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TextView
        android:id="@+id/content"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:text="Here comes the tab content"
        android:textColor="#FFEEEEEE"/>
</LinearLayout>
Code:
public class AboutActivity extends MyAbstractActivity {

    @Override
    protected String getContent() {
        return "About info";
    }
}
A more complex example of an Activity with tab switching mechanics:
Layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical">
    <TextView
        android:id="@+id/content"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="Here comes the tab content"
        android:textColor="#FFEEEEEE"/>
    <Button
        android:id="@+id/bttn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="ClickMe"/>
</LinearLayout>
Code:
public class BlogsActivity extends MyAbstractActivity {

    @Override
    protected void setContent() {
        setContentView(R.layout.content_bttn);
        ((TextView) findViewById(R.id.content)).setText(getContent());
        ((Button) findViewById(R.id.bttn)).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (getParent() instanceof Main) {
                    ((Main) getParent()).getTabHost().setCurrentTab(Main.SINGLEBLOG_TAB);
                    Main.SELECTED_BLOGS_TAB = Main.SINGLEBLOG_TAB;
                }
            }
        });
    }

    @Override
    protected String getContent() {
        return "Fancy blogs list";
    }
}
Switching mechanics can also be externalized into a separate Singleton that has a reference to the TabHost.

This ends the example of how to use the TabHost a bit more freely.

Icons from http://www.icojoy.com

Complete Android Project for Eclipse: CustomTabsDemo

Keine Kommentare:

Kommentar veröffentlichen