I’ve been recently using the Android Compatibility Library in a project where I found myself in the need for a tabbed layout, in which each of the tab’s children would be a fragment. The usual way to do this is for the activity you need the tabs in to extend TabActivity. However, if you want to use fragments in your activity, you must extend android.support.v4.app.FragmentActivity instead. Since Java doesn’t allow multiple class inheritance, you find yourself in a bit of dilemma.
Well, there’s a simple solution for making this work. What you need is basically to use a TabHost to hold both the TabWidget (which are the actual tab labels) and their content. Since the TabHost is nothing more than a View, you can place that anywhere, including in the root of a fragment defined in XML, like the one below (tabs_fragment.xml). I am hoping the comments in the code are self-explanatory enough.
<?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" android:background="#EFEFEF"> <LinearLayout android:orientation="vertical" 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" /> <FrameLayout android:id="@android:id/tabcontent" android:layout_width="fill_parent" android:layout_height="fill_parent"> <FrameLayout android:id="@+id/tab_1" android:layout_width="fill_parent" android:layout_height="fill_parent" /> <FrameLayout android:id="@+id/tab_2" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </FrameLayout> </LinearLayout> </TabHost>
Don’t forget to use the correct ids (@android:id/tabhost, @android:id/tabs and @android:id/tabcontent) because the TabHost expects exactly these in order to retrieve the references for each component of the view. If you want to use a custom style for your tabs, it is easy to do so by creating a tab_selector.xml in /res/drawables in which you can use different colors or backgrounds for each tab state. You can find an example in the attached zip.
The equivalent code for actually creating the tabs is in TabsFragment.java below.
package org.codeandmagic.android;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TabHost;
import android.widget.TabHost.OnTabChangeListener;
import android.widget.TabHost.TabSpec;
import android.widget.TextView;
public class TabsFragment extends Fragment implements OnTabChangeListener {
private static final String TAG = "FragmentTabs";
public static final String TAB_WORDS = "words";
public static final String TAB_NUMBERS = "numbers";
private View mRoot;
private TabHost mTabHost;
private int mCurrentTab;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mRoot = inflater.inflate(R.layout.tabs_fragment, null);
mTabHost = (TabHost) mRoot.findViewById(android.R.id.tabhost);
setupTabs();
return mRoot;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setRetainInstance(true);
mTabHost.setOnTabChangedListener(this);
mTabHost.setCurrentTab(mCurrentTab);
// manually start loading stuff in the first tab
updateTab(TAB_WORDS, R.id.tab_1);
}
private void setupTabs() {
mTabHost.setup(); // you must call this before adding your tabs!
mTabHost.addTab(newTab(TAB_WORDS, R.string.tab_words, R.id.tab_1));
mTabHost.addTab(newTab(TAB_NUMBERS, R.string.tab_numbers, R.id.tab_2));
}
private TabSpec newTab(String tag, int labelId, int tabContentId) {
Log.d(TAG, "buildTab(): tag=" + tag);
View indicator = LayoutInflater.from(getActivity()).inflate(
R.layout.tab,
(ViewGroup) mRoot.findViewById(android.R.id.tabs), false);
((TextView) indicator.findViewById(R.id.text)).setText(labelId);
TabSpec tabSpec = mTabHost.newTabSpec(tag);
tabSpec.setIndicator(indicator);
tabSpec.setContent(tabContentId);
return tabSpec;
}
@Override
public void onTabChanged(String tabId) {
Log.d(TAG, "onTabChanged(): tabId=" + tabId);
if (TAB_WORDS.equals(tabId)) {
updateTab(tabId, R.id.tab_1);
mCurrentTab = 0;
return;
}
if (TAB_NUMBERS.equals(tabId)) {
updateTab(tabId, R.id.tab_2);
mCurrentTab = 1;
return;
}
}
private void updateTab(String tabId, int placeholder) {
FragmentManager fm = getFragmentManager();
if (fm.findFragmentByTag(tabId) == null) {
fm.beginTransaction()
.replace(placeholder, new MyListFragment(tabId), tabId)
.commit();
}
}
}
Now that you have the content of your fragment in place, you need to create the tab’s children (which are fragments, like the title says). You can subclass the android.support.v4.app.Fragment (I used a ListFragment in this example). You can also use loaders if your fragment needs to do some background work. In the example below, I use an AsyncTaskLoader to simulate a time-consuming operation in background, at the end of which some words or numbers are added into the list’s adapter and displayed inside a tab. You also need a LoaderCallback object (or your fragment can directly implement this interface) in order to communicate with the LoaderManager to create your loader and also know when its job is done.
package org.codeandmagic.android;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
public class MyListFragment extends ListFragment implements
LoaderCallbacks<Void> {
private static final String TAG = "FragmentTabs";
private String mTag;
private MyAdapter mAdapter;
private ArrayList<String> mItems;
private LayoutInflater mInflater;
private int mTotal;
private int mPosition;
private static final String[] WORDS = { "Lorem", "ipsum", "dolor", "sit",
"amet", "consectetur", "adipiscing", "elit", "Fusce", "pharetra",
"luctus", "sodales" };
private static final String[] NUMBERS = { "I", "II", "III", "IV", "V",
"VI", "VII", "VIII", "IX", "X", "XI", "XII", "XIII", "XIV", "XV" };
private static final int SLEEP = 1000;
private final int wordBarColor = R.color.word_bar;
private final int numberBarColor = R.color.number_bar;
public MyListFragment() {
}
public MyListFragment(String tag) {
mTag = tag;
mTotal = TabsFragment.TAB_WORDS.equals(mTag) ? WORDS.length
: NUMBERS.length;
Log.d(TAG, "Constructor: tag=" + tag);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// this is really important in order to save the state across screen
// configuration changes for example
setRetainInstance(true);
mInflater = LayoutInflater.from(getActivity());
// you only need to instantiate these the first time your fragment is
// created; then, the method above will do the rest
if (mAdapter == null) {
mItems = new ArrayList<String>();
mAdapter = new MyAdapter(getActivity(), mItems);
}
getListView().setAdapter(mAdapter);
// initiate the loader to do the background work
getLoaderManager().initLoader(0, null, this);
}
@Override
public Loader<Void> onCreateLoader(int id, Bundle args) {
AsyncTaskLoader<Void> loader = new AsyncTaskLoader<Void>(getActivity()) {
@Override
public Void loadInBackground() {
try {
// simulate some time consuming operation going on in the
// background
Thread.sleep(SLEEP);
} catch (InterruptedException e) {
}
return null;
}
};
// somehow the AsyncTaskLoader doesn't want to start its job without
// calling this method
loader.forceLoad();
return loader;
}
@Override
public void onLoadFinished(Loader<Void> loader, Void result) {
// add the new item and let the adapter know in order to refresh the
// views
mItems.add(TabsFragment.TAB_WORDS.equals(mTag) ? WORDS[mPosition]
: NUMBERS[mPosition]);
mAdapter.notifyDataSetChanged();
// advance in your list with one step
mPosition++;
if (mPosition < mTotal - 1) {
getLoaderManager().restartLoader(0, null, this);
Log.d(TAG, "onLoadFinished(): loading next...");
} else {
Log.d(TAG, "onLoadFinished(): done loading!");
}
}
@Override
public void onLoaderReset(Loader<Void> loader) {
}
private class MyAdapter extends ArrayAdapter<String> {
public MyAdapter(Context context, List<String> objects) {
super(context, R.layout.list_item, R.id.text, objects);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
Wrapper wrapper;
if (view == null) {
view = mInflater.inflate(R.layout.list_item, null);
wrapper = new Wrapper(view);
view.setTag(wrapper);
} else {
wrapper = (Wrapper) view.getTag();
}
wrapper.getTextView().setText(getItem(position));
wrapper.getBar().setBackgroundColor(
mTag == TabsFragment.TAB_WORDS ? getResources().getColor(
wordBarColor) : getResources().getColor(
numberBarColor));
return view;
}
}
// use an wrapper (or view holder) object to limit calling the
// findViewById() method, which parses the entire structure of your
// XML in search for the ID of your view
private class Wrapper {
private final View mRoot;
private TextView mText;
private View mBar;
public Wrapper(View root) {
mRoot = root;
}
public TextView getTextView() {
if (mText == null) {
mText = (TextView) mRoot.findViewById(R.id.text);
}
return mText;
}
public View getBar() {
if (mBar == null) {
mBar = mRoot.findViewById(R.id.bar);
}
return mBar;
}
}
}
Now, all you need to do is create a FragmentActivity to accommodate your tabs fragment. You can either instantiate the fragment programmatically using the methods in the FragmentManager or by putting it directly in XML, like below (main.xml).
<?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="fill_parent"> <fragment class="org.codeandmagic.android.TabsFragment" android:id="@+id/tabs_fragment" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </LinearLayout>
With that, your job is pretty much done. You can see in the screenshot the result and also can download the project that’s available in the attached zip. Enjoy!


Pingback: The Android Tutorial Series Part 3 – Layout | My Experiments with Technology
Pingback: Android Tabs with Fragments « tediscript.wordpress.com
Pingback: Android U_MISSING_RESOURCE_ERROR when populating a custom view in a ListFragment | PHP Developer Resource
Pingback: Android Fragments con pestañas | LiME Creative LabsLiME Creative Labs
Pingback: How to make tabs using fragments (using content by factory)? : Android Community - For Application Development
Pingback: Project for tabs using fragments not working video