On my last project, I wanted to design one of the screens to look like Google Newsstand’s swiping tabs that were also scrollable and had an image between the ActionBar and the tabs.
I did not find any simple and useful solution to this that I could easily follow and implement in my app. So I created my own. Here’s how it looks like.
Google recommends using FragmentTabHost in place of deprecated TabHost. FragmentTabHost usually has two layout components, TabWidget (for tabs) and FrameLayout (for tabContent). To use ViewPager, I added the Tab Content layout with height 0dp and added another FrameLayout that will host the ViewPager, like this:
The Fragments
Let’s start by creating a fragment that we will reuse for our tabs. Here’s the fragment layout
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.intelliabb.hnabbasi.fragmenthostviewpager.Fragments.MyFragment"> <TextView android:id="@+id/label" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:textSize="20dp"/> </FrameLayout>
The Fragment class that uses this layout,
public class MyFragment extends Fragment { String label; public static MyFragment newInstance(String fragmentLabel) { MyFragment fragment = new MyFragment(); Bundle args = new Bundle(); args.putString("label", fragmentLabel); fragment.setArguments(args); return fragment; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_item, container, false); TextView tv = (TextView) view.findViewById(R.id.label); //If the fragment was created by the TabHost, return empty view if(getArguments() == null) return view; label = getArguments().getString("label", ""); tv.setText(label); return view; } }
Since we are using two different methods to create this fragment (one from the FragmentTabHost and one from the ViewPager), we will check if the bundle passed to the fragment while creating it’s instance is null. If it is, then we will simply return the view without populating it with any content.
FragmentTabHost Layout
Now that we have the fragments ready, let’s configure the FragmentHost to use these fragments. In this layout, we will have FragmentTabHost with tabs, tabcontent, and realtabcontent components. Since we want our ViewPager to actually create the views and use the fragments inside realtabcontent, we will set the height of tabscontent to 0dp. The fragments will return an empty view if the bundle is empty and hence we will not have any view created on the screen.
<android.support.v4.app.FragmentTabHost android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingBottom="@dimen/activity_vertical_margin" android:id="@android:id/tabhost" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@id/header_content"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TabWidget android:id="@android:id/tabs" android:showDividers="none" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_weight="0"/> <FrameLayout android:id="@android:id/tabcontent" android:layout_width="0dp" android:layout_height="0dp" android:layout_weight="0"/> <FrameLayout android:id="@+id/realTabContent" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="0"/> </LinearLayout> </android.support.v4.app.FragmentTabHost>
ViewPager Layout
Now, the ViewPager where all the action is. We will use the ViewPagerAdapter to request and new instances of the Fragments and pass the appropriate bundle. ViewPager will reside inside the FragmentTabHost and has a simple layout,
<android.support.v4.view.ViewPager android:id="@+id/pager" android:layout_width="match_parent" android:layout_height="match_parent"> </android.support.v4.view.ViewPager>
ViewPagerAdapter
ViewPager get’s its content (sliding views) from a PagerAdapter. In our example, we will use a custom adapter called ViewPagerAdapter that extends FragmentPagerAdapter,
public class ViewPagerAdapter extends FragmentPagerAdapter { String[] tabs; public ViewPagerAdapter(FragmentManager fm, String[] tabs) { super(fm); this.tabs = tabs; } @Override public Fragment getItem(int i) { return MyFragment.newInstance(String.format("%s Fragment", tabs[i])); } @Override public int getCount() { return tabs.length; } }
The Activity
Now that we have all the individual parts ready, we will start linking them in the activity. Our MainActivity extends FragmentActivity so we can get the Fragment manager for our ViewPager.
public class MainActivity extends FragmentActivity
In the onCreate() method of our activity, we will initialize the ViewPager, TabHost, and TabHost
viewPager = (ViewPager) findViewById(R.id.pager); tabHost = (FragmentTabHost) findViewById(android.R.id.tabhost); tabWidget = (TabWidget) findViewById(android.R.id.tabs); tabHost.setup(this, getSupportFragmentManager(), R.id.realTabContent);
We will get the data in our activity and pass each fragment it’s data to display.
Tabs:
TV Shows
Movies
Music
News
Weather
Pass each tab’s content via ViewPagerTabAdapter. We will initialize the tabs,
private void initializeTabs() { tabs = new String[] { "TV Shows", "Movies", "Music", "News", "Weather" }; }
Now, we will setup the TabHost,
private void setupTabHost() { for(int i=0; i<tabs.length; i++) { tabHost.addTab(tabHost.newTabSpec(String.format("%sTab", tabs[i].replace(" ","").toLowerCase())).setIndicator(tabs[i]), MyFragment.class, null); } }
Now that we have the tabs ready. We will initialize the PagerAdapter and set it to ViewPager,
pagerAdapter = new ViewPagerAdapter(getSupportFragmentManager(), tabs); viewPager.setAdapter(pagerAdapter);
We will handle setOnPageChangedListener() of the ViewPager to communicate to the TabHost to switch to the corresponding tab when swiping through pages,
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageSelected(int position) { invalidateOptionsMenu(); tabHost.setCurrentTab(position); } });
Similarly, we have to handle the TabHost setOnTabChagedListener() to communicate to the ViewPager to swipe to corresponding page,
tabHost.setOnTabChangedListener(new TabHost.OnTabChangeListener() { @Override public void onTabChanged(String tabId) { viewPager.setCurrentItem(tabHost.getCurrentTab()); } });
This completes our setup. But, we are not done quite yet…
Scrolling Tabs
To let the user scroll the tabs and center the tab on selection, we will do a minor tweaking using ScrollTo() in HorizontalScrollableView.
private void scrollToCurrentTab() { final int screenWidth = getWindowManager().getDefaultDisplay().getWidth(); final int leftX = tabWidget.getChildAt(tabHost.getCurrentTab()).getLeft(); int newX = 0; newX = leftX + (tabWidget.getChildAt(tabHost.getCurrentTab()).getWidth() / 2) - (screenWidth / 2); if (newX < 0) { newX = 0; } horizontalScrollView.scrollTo(newX, 0); }
Now, we are done with our setup. This is how the completed MainActivity looks like,
/* * FragmentHost with ViewPager * By Hussain Abbasi at intelliAbb.com */ package com.intelliabb.hnabbasi.fragmenthostviewpager; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentTabHost; import android.support.v4.view.ViewPager; import android.widget.FrameLayout; import android.widget.HorizontalScrollView; import android.widget.LinearLayout; import android.widget.TabHost; import android.widget.TabWidget; import com.intelliabb.hnabbasi.fragmenthostviewpager.Adapters.ViewPagerAdapter; import com.intelliabb.hnabbasi.fragmenthostviewpager.Fragments.MyFragment; public class MainActivity extends FragmentActivity { private String[] tabs; FragmentTabHost tabHost; ViewPagerAdapter pagerAdapter; ViewPager viewPager; private TabWidget tabWidget; private HorizontalScrollView horizontalScrollView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.requestWindowFeature(getWindow().FEATURE_NO_TITLE); setContentView(R.layout.activity_main); viewPager = (ViewPager) findViewById(R.id.pager); tabHost = (FragmentTabHost) findViewById(android.R.id.tabhost); tabWidget = (TabWidget) findViewById(android.R.id.tabs); tabHost.setup(this, getSupportFragmentManager(), R.id.realTabContent); initializeHorizontalTabs(); initializeTabs(); setupTabHost(); pagerAdapter = new ViewPagerAdapter(getSupportFragmentManager(), tabs); viewPager.setAdapter(pagerAdapter); viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { /** * on swipe select the respective tab * */ @Override public void onPageSelected(int position) { invalidateOptionsMenu(); tabHost.setCurrentTab(position); } @Override public void onPageScrolled(int arg0, float arg1, int arg2) { } @Override public void onPageScrollStateChanged(int arg0) { invalidateOptionsMenu(); } }); tabHost.setOnTabChangedListener(new TabHost.OnTabChangeListener() { @Override public void onTabChanged(String tabId) { viewPager.setCurrentItem(tabHost.getCurrentTab()); scrollToCurrentTab(); } }); } private void initializeHorizontalTabs() { LinearLayout ll = (LinearLayout) tabWidget.getParent(); horizontalScrollView = new HorizontalScrollView(this); horizontalScrollView.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT)); ll.addView(horizontalScrollView, 0); ll.removeView(tabWidget); horizontalScrollView.addView(tabWidget); horizontalScrollView.setHorizontalScrollBarEnabled(false); } private void scrollToCurrentTab() { final int screenWidth = getWindowManager().getDefaultDisplay().getWidth(); final int leftX = tabWidget.getChildAt(tabHost.getCurrentTab()).getLeft(); int newX = 0; newX = leftX + (tabWidget.getChildAt(tabHost.getCurrentTab()).getWidth() / 2) - (screenWidth / 2); if (newX < 0) { newX = 0; } horizontalScrollView.scrollTo(newX, 0); } private void initializeTabs() { tabs = new String[] { "TV Shows", "Movies", "Music", "News", "Weather" }; } private void setupTabHost() { for(int i=0; i<tabs.length; i++) { tabHost.addTab(tabHost.newTabSpec(String.format("%sTab", tabs[i].replace(" ","").toLowerCase())).setIndicator(tabs[i]), MyFragment.class, null); } } }
Wrap Up
I am sure there may be a better way to implement this, but after searching for one online and in Android docs, I could not find one. This is simple and gets the job done.
GitHub: https://github.com/Intelliabb/android/tree/master/FragmentHostViewPager
Leave your suggestions and comments below in the comment section. Enjoy.
Hi,How can I set different fragment to each tab?
In this example, I am reusing MyFragment for all tabs. To get a different fragment for each tab, you would initialize it when adding tabs to TabHost.
For example,
I am getting a nullpointerException at this line.
tabHost.setup(this, getSupportFragmentManager(), R.id.realTabContent);
Please help me. I just copied your code from github and tried to run in my system. Thanking you.
Did you clone the repo or just copy paste? Seems like your `tabHost` is not initialized. Make sure to clone the repo so you also get the layout with `tabHost` in it.