time

How to organize content in tabs with TabLayout and ViewPager in Android

Earlier to make tabs in Android, ActionBar was used. But now with API 21 onwards it has been deprecated. The new Design Support Library has many new UI components like floating action buttons, snackbars and tabs.

The base componets of this tutorial are

  • ViewPager. The primary role of the ViewPager is to allow the user to flip through different pages of information where each page is most typically represented by a layout fragment. The fragments that are associated with the ViewPager are managed by an instance of the FragmentStatePagerAdapter class. At a minimum the pager adapter assigned to a ViewPager must implement two methods. The first, named getCount(), must return the total number of page fragments available to be displayed to the user. The second method, getItem(), is passed a page number and must return the corresponding fragment object ready to be presented to the user.
  • TabLayout is one of the components introduced as part of material design and is included in the design support library. The purpose of the TabLayout is to present the user with a row of tabs which can be selected to display different pages to the user.
android_tablayout.png

To start off include following libraries in the dependencies section of your build.gradle file

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:26.1.0'
    compile 'com.android.support:design:26.1.0'
}

We will be using Android Toolbar and TabLayout classes to show tabs, lets remove the ActionBar from layout by using styles. In order to change the theme, go to app - res – values – styles and change the default theme Theme.AppCompat.Light.DarkActionBar to Theme.AppCompat.Light.NoActionBar.

# file styles.xml
<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="colorPrimary">#3F51B5</item>
        <item name="colorPrimaryDark">#303F9F</item>
        <item name="colorAccent">#FF4081</item>
    </style>
</resources>

Next, to display Android tabs with fragment and ViewPager, lets define simple fragment

# file TabFragment.java

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.Toast;

public class TabFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.tab_fragment, container, false);
        Button btnIntroduce = (Button) view.findViewById(R.id.btnIntroduce);
        btnIntroduce.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getActivity(), "Fragment", Toast.LENGTH_LONG).show();
            }
        });        

        return view;
    }
}

and layout

# file tab_fragment.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Tab 1"
        android:textAppearance="?android:attr/textAppearanceLarge"/>

    <Button
        android:id="@+id/btnIntroduce"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Who I am?"
        android:layout_below="@+id/textView"
        android:layout_centerHorizontal="true"        
        />

</RelativeLayout>

Now that we have all the tab fragments defined, lets define a ViewPager adapter for the swipe tabs feature

# file PagerAdapter.java

import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;

public class PagerAdapter extends FragmentStatePagerAdapter {
    int numTabs;

    public PagerAdapter(FragmentManager fm, int numTabs) {
        super(fm);
        this.numTabs = numTabs;
    }

    @Override
    public Fragment getItem(int position) {
        switch (position) {
            case 0:
                TabFragment tab1 = new TabFragment();
                return tab1;
            case 1:
                TabFragment tab2 = new TabFragment();
                return tab2;
            case 2:
                TabFragment tab3 = new TabFragment();
                return tab3;
            default:
                return null;
        }
    }

    @Override
    public int getCount() {
        return numTabs;
    }
}

At the very least, subclasses of FragmentStatePagerAdapter must implement getItem() and getCount().

  • getItem() is expected to return the fragment for the specified position. Internally, FragmentStatePagerAdapter calls getItem() to instantiate fragments, and uses the FragmentManager passed into the constructor to manage these fragments.
  • getCount() should return the total number of fragments.

We can override getPageTitle(), which is used to set the text for each tab position.

Next lets define the layout for main activity where all these tabs would be displayed.

# file activity_main.xml

<RelativeLayout
    android:id="@+id/main_layout"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:background="?attr/colorPrimary"
        android:elevation="6dp"
        android:minHeight="?attr/actionBarSize"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

    <android.support.design.widget.TabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/toolbar"
        android:background="?attr/colorPrimary"
        android:elevation="6dp"
        android:minHeight="?attr/actionBarSize"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"/>

    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="fill_parent"
        android:layout_below="@id/tab_layout"/>

</RelativeLayout>

The remaining tasks involve initializing the TabLayout, ViewPager and PagerAdapter instances. All of these tasks will be performed in the onCreate() method of the MainActivity.java file.

# file MainActivity.java

import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;


public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout);
        tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
        tabLayout.addTab(tabLayout.newTab().setText("Tab 2"));
        tabLayout.addTab(tabLayout.newTab().setText("Tab 3"));
        tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);

        final ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
        final PagerAdapter adapter = new PagerAdapter
                (getSupportFragmentManager(), tabLayout.getTabCount());
        viewPager.setAdapter(adapter);
        viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));

        // This method ensures that tab selection events update the ViewPager and page changes update the selected tab.
        // tabLayout.setupWithViewPager(viewPager);

        tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                viewPager.setCurrentItem(tab.getPosition());
            }

            @Override
            public void onTabUnselected(TabLayout.Tab tab) {}
            @Override
            public void onTabReselected(TabLayout.Tab tab) {}
        });
    }
}

Code on github.

Changing size of tab title

In an effort to fit the tabs into the available display width the TabLayout has used multiple lines of text. Even so, the second line is clearly truncated making it impossible to see the full title. The best solution to this problem is to switch the TabLayout to scrollable mode. In this mode the titles appear in full length, single line format allowing the user to swipe to scroll horizontally through the available items.

To switch a TabLayout to scrollable mode, simply change the app:tabMode property in the activity_main.xml layout resource file from fixed to scrollable

<android.support.design.widget.TabLayout
    android:id="@+id/tab_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:tabMode="scrollable"
    app:tabGravity="fill"/>
</android.support.design.widget.AppBarLayout>

When in fixed mode, the TabLayout may be configured to control how the tab items are displayed to take up the available space on the screen. This is controlled via the app:tabGravity property, the results of which are more noticeable on wider displays such as tablets in landscape orientation. When set to fill, for example, the items will be distributed evenly across the width of the TabLayout.

Changing text size of tab items

To replace the text size of tabs items create new style in style.xml

<style name="CustomTabText" parent="TextAppearance.Design.Tab">
    <item name="android:textSize">16sp</item>
</style>

Use is in TabLayout like this

<android.support.design.widget.TabLayout
    app:tabTextAppearance="@style/CustomTabText"
    ...
    />

Displaying icon tab items

To replace the text based tabs with icons, modify the onCreate() method in the MainActivity.java file to assign some built-in drawable icons to the tab items

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout);
    tabLayout.addTab(tabLayout.newTab().setIcon(
            android.R.drawable.ic_dialog_email));
    tabLayout.addTab(tabLayout.newTab().setIcon(
            android.R.drawable.ic_dialog_dialer));
    tabLayout.addTab(tabLayout.newTab().setIcon(
            android.R.drawable.ic_dialog_map));
    ...
}

TabLayout and Kotlin

In the following sections, we'll dive into coding a simple app that makes use of TabLayout with a ViewPager.

To be able to follow this section, you'll need:

  • Android Studio 3.0 or higher
  • Kotlin plugin 1.1.51 or higher

You can also learn all the ins and outs of the Kotlin language here.

We're going to create a TabLayout with just three tabs. When each of the tabs is selected, it displays a different Android fragment or page. So let's now create the three Android fragments for each of the tabs. We'll start with the first fragment class, and you should follow a similar process for the remaining two fragment classes - FragmentTwo.kt and FragmentThree.kt.

Here is my FragmentOne.kt

class FragmentOne : Fragment() {
    companion object {
        fun newInstance(): FragmentOne = FragmentOne()
    }

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
        savedInstanceState: Bundle?): View? =
            inflater!!.inflate(R.layout.fragment_one, container, false)
}

Here is also my R.layout.fragment_one.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Tab 1"
        android:textAppearance="?android:attr/textAppearanceLarge"/>

</RelativeLayout>

Also, visit your res/layout/activlty_main.xml file to include both the TabLayout widget and the ViewPager view.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    android:id="@+id/main_layout"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.design.widget.TabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:background="?attr/colorPrimary"
        android:elevation="6dp"
        android:minHeight="?attr/actionBarSize"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"/>

    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="fill_parent"
        android:layout_below="@id/tab_layout"/>

</RelativeLayout>

Here we created a simple TabLayout with id tab_layout.

We need to create a subclass in SampleAdapter.kt that extends the FragmentPagerAdapter. This class is responsible for managing the different fragments that will be displayed on the tabs.

class SampleAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {

    override fun getItem(position: Int): Fragment? = when (position) {
        0 -> FragmentOne.newInstance()
        1 -> FragmentTwo.newInstance()
        2 -> FragmentThree.newInstance()
        else -> null
    }

    override fun getPageTitle(position: Int): CharSequence = when (position) {
        0 -> "Tab 1"
        1 -> "Tab 2"
        2 -> "Tab 3"
        else -> ""
    }

    override fun getCount(): Int = 3
}

Here we override three methods from the parent class: getItem(), getCount(), and getPageTitle(). Here are the explanations for the methods:

  • getItem(): returns a Fragment for a particular position within the ViewPager.
  • getCount(): indicates how many pages will be in the ViewPager.
  • getPageTitle(): this method is called by the ViewPager to obtain a title string to describe the specified tab.

Next, we are going to initialize instances of our TabLayout, ViewPager, and SampleAdapter. Initialization is going to happen inside onCreate() in MainActivity.kt.

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val tabLayout: TabLayout = findViewById(R.id.tab_layout)
        val viewPager: ViewPager = findViewById(R.id.pager)

        val adapter = SampleAdapter(supportFragmentManager)

        viewPager.adapter = adapter
        tabLayout.setupWithViewPager(viewPager)
    }
}

We got references to our TabLayout and ViewPager from R.layout.activity_main and initialized them. We also created an instance of our SampleAdapter - passing an instance of FragmentManager as an argument. We need to supply the views for our ViewPager, so we called setAdapter() and passed in our created adapter to it. Finally, we called setupWithViewPager() on an instance of TabLayout to do some work: creation of the required tab for every page and setting up the required listeners.

When the user taps on a tab, it changes the pages in the ViewPager and shows the required page (or Fragment). Also, swiping between pages updates the selected tab. In other words, this method helps us take care of scroll state change and clicks on the tabs.

Note that if you want to explicitly create the tabs instead of using the helper method setUpWithViewPager(), you can instead use newTab() on a TabLayout instance.

val tabLayout: TabLayout = findViewById(R.id.tab_layout)

tabLayout.addTab(tabLayout.newTab().setText("Songs"))
tabLayout.addTab(tabLayout.newTab().setText("Albums"))
tabLayout.addTab(tabLayout.newTab().setText("Artists"))

Note also that we could explicitly create the tabs via XML instead of programmatically.

<android.support.design.widget.TabLayout
        android:id="@+id/tabs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    <android.support.design.widget.TabItem
            android:id="@+id/tabItem"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Songs"/>

    <android.support.design.widget.TabItem
            android:id="@+id/tabItem2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Albums"/>

    <android.support.design.widget.TabItem
            android:id="@+id/tabItem3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Artists"/>

</android.support.design.widget.TabLayout>