I'm trying to implement tabs for navigation in an Android app. Since TabActivity and ActivityGroup are deprecated I would like to implement it using Fragments instead.
I know how to set up one fragment for each tab and then switch fragments when a tab is clicked. But how can I have a separate back stack for each tab?
For an example Fragment A and B would be under Tab 1 and Fragment C and D under Tab 2. When the app is started Fragment A is shown and Tab 1 is selected. Then Fragment A might be replaced with Fragment B. When Tab 2 is selected Fragment C should be displayed. If Tab 1 is then selected Fragment B should once again be displayed. At this point it should be possible to use the back button to show Fragment A.
Also, it is important that the state for each tab is maintained when the device is rotated.
BR Martin
I am terribly late to this question . But since this thread has been very informative and helpful to me I thought I better post my two pence here.
I needed a screen flow like this (A minimalistic design with 2 tabs and 2 views in each tab),
tabA
-> ScreenA1, ScreenA2
tabB
-> ScreenB1, ScreenB2
I had the same requirements in the past, and I did it using TabActivityGroup
(which was deprecated at that time too) and Activities. This time I wanted to use Fragments.
So this is how I done it.
public class BaseFragment extends Fragment {
AppMainTabActivity mActivity;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mActivity = (AppMainTabActivity) this.getActivity();
}
public void onBackPressed(){
}
public void onActivityResult(int requestCode, int resultCode, Intent data){
}
}
All fragments in your app can extend this Base class. If you want to use special fragments like ListFragment
you should create a base class for that too. You will be clear about the usage of onBackPressed()
and onActivityResult()
if you read the post in full..
public class AppConstants{
public static final String TAB_A = "tab_a_identifier";
public static final String TAB_B = "tab_b_identifier";
//Your other constants, if you have them..
}
nothing to explain here..
public class AppMainFragmentActivity extends FragmentActivity{
/* Your Tab host */
private TabHost mTabHost;
/* A HashMap of stacks, where we use tab identifier as keys..*/
private HashMap<String, Stack<Fragment>> mStacks;
/*Save current tabs identifier in this..*/
private String mCurrentTab;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.app_main_tab_fragment_layout);
/*
* Navigation stacks for each tab gets created..
* tab identifier is used as key to get respective stack for each tab
*/
mStacks = new HashMap<String, Stack<Fragment>>();
mStacks.put(AppConstants.TAB_A, new Stack<Fragment>());
mStacks.put(AppConstants.TAB_B, new Stack<Fragment>());
mTabHost = (TabHost)findViewById(android.R.id.tabhost);
mTabHost.setOnTabChangedListener(listener);
mTabHost.setup();
initializeTabs();
}
private View createTabView(final int id) {
View view = LayoutInflater.from(this).inflate(R.layout.tabs_icon, null);
ImageView imageView = (ImageView) view.findViewById(R.id.tab_icon);
imageView.setImageDrawable(getResources().getDrawable(id));
return view;
}
public void initializeTabs(){
/* Setup your tab icons and content views.. Nothing special in this..*/
TabHost.TabSpec spec = mTabHost.newTabSpec(AppConstants.TAB_A);
mTabHost.setCurrentTab(-3);
spec.setContent(new TabHost.TabContentFactory() {
public View createTabContent(String tag) {
return findViewById(R.id.realtabcontent);
}
});
spec.setIndicator(createTabView(R.drawable.tab_home_state_btn));
mTabHost.addTab(spec);
spec = mTabHost.newTabSpec(AppConstants.TAB_B);
spec.setContent(new TabHost.TabContentFactory() {
public View createTabContent(String tag) {
return findViewById(R.id.realtabcontent);
}
});
spec.setIndicator(createTabView(R.drawable.tab_status_state_btn));
mTabHost.addTab(spec);
}
/*Comes here when user switch tab, or we do programmatically*/
TabHost.OnTabChangeListener listener = new TabHost.OnTabChangeListener() {
public void onTabChanged(String tabId) {
/*Set current tab..*/
mCurrentTab = tabId;
if(mStacks.get(tabId).size() == 0){
/*
* First time this tab is selected. So add first fragment of that tab.
* Dont need animation, so that argument is false.
* We are adding a new fragment which is not present in stack. So add to stack is true.
*/
if(tabId.equals(AppConstants.TAB_A)){
pushFragments(tabId, new AppTabAFirstFragment(), false,true);
}else if(tabId.equals(AppConstants.TAB_B)){
pushFragments(tabId, new AppTabBFirstFragment(), false,true);
}
}else {
/*
* We are switching tabs, and target tab is already has atleast one fragment.
* No need of animation, no need of stack pushing. Just show the target fragment
*/
pushFragments(tabId, mStacks.get(tabId).lastElement(), false,false);
}
}
};
/* Might be useful if we want to switch tab programmatically, from inside any of the fragment.*/
public void setCurrentTab(int val){
mTabHost.setCurrentTab(val);
}
/*
* To add fragment to a tab.
* tag -> Tab identifier
* fragment -> Fragment to show, in tab identified by tag
* shouldAnimate -> should animate transaction. false when we switch tabs, or adding first fragment to a tab
* true when when we are pushing more fragment into navigation stack.
* shouldAdd -> Should add to fragment navigation stack (mStacks.get(tag)). false when we are switching tabs (except for the first time)
* true in all other cases.
*/
public void pushFragments(String tag, Fragment fragment,boolean shouldAnimate, boolean shouldAdd){
if(shouldAdd)
mStacks.get(tag).push(fragment);
FragmentManager manager = getSupportFragmentManager();
FragmentTransaction ft = manager.beginTransaction();
if(shouldAnimate)
ft.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left);
ft.replace(R.id.realtabcontent, fragment);
ft.commit();
}
public void popFragments(){
/*
* Select the second last fragment in current tab's stack..
* which will be shown after the fragment transaction given below
*/
Fragment fragment = mStacks.get(mCurrentTab).elementAt(mStacks.get(mCurrentTab).size() - 2);
/*pop current fragment from stack.. */
mStacks.get(mCurrentTab).pop();
/* We have the target fragment in hand.. Just show it.. Show a standard navigation animation*/
FragmentManager manager = getSupportFragmentManager();
FragmentTransaction ft = manager.beginTransaction();
ft.setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right);
ft.replace(R.id.realtabcontent, fragment);
ft.commit();
}
@Override
public void onBackPressed() {
if(mStacks.get(mCurrentTab).size() == 1){
// We are already showing first fragment of current tab, so when back pressed, we will finish this activity..
finish();
return;
}
/* Each fragment represent a screen in application (at least in my requirement, just like an activity used to represent a screen). So if I want to do any particular action
* when back button is pressed, I can do that inside the fragment itself. For this I used AppBaseFragment, so that each fragment can override onBackPressed() or onActivityResult()
* kind of events, and activity can pass it to them. Make sure just do your non navigation (popping) logic in fragment, since popping of fragment is done here itself.
*/
((AppBaseFragment)mStacks.get(mCurrentTab).lastElement()).onBackPressed();
/* Goto previous fragment in navigation stack of this tab */
popFragments();
}
/*
* Imagine if you wanted to get an image selected using ImagePicker intent to the fragment. Ofcourse I could have created a public function
* in that fragment, and called it from the activity. But couldn't resist myself.
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(mStacks.get(mCurrentTab).size() == 0){
return;
}
/*Now current fragment on screen gets onActivityResult callback..*/
mStacks.get(mCurrentTab).lastElement().onActivityResult(requestCode, resultCode, data);
}
}
<?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">
<LinearLayout
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<FrameLayout
android:id="@android:id/tabcontent"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="0"/>
<FrameLayout
android:id="@+android:id/realtabcontent"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<TabWidget
android:id="@android:id/tabs"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="0"/>
</LinearLayout>
</TabHost>
public class AppTabAFragment extends BaseFragment {
private Button mGotoButton;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_one_layout, container, false);
mGoToButton = (Button) view.findViewById(R.id.goto_button);
mGoToButton.setOnClickListener(listener);
return view;
}
private OnClickListener listener = new View.OnClickListener(){
@Override
public void onClick(View v){
/* Go to next fragment in navigation stack*/
mActivity.pushFragments(AppConstants.TAB_A, new AppTabAFragment2(),true,true);
}
}
}
This might not be the most polished and correct way. But it worked beautifully in my case. Also I only had this requirement in portrait mode. I never had to use this code in a project supporting both orientation. So can't say what kind of challenges I face there..
If anyone want a full project, I have pushed a sample project to github.