In this tutorial, we’ll be implementing android nested ViewPager i.e a ViewPager within a ViewPager. Also, we’ll discuss and implement Vertical ViewPager too. We’ll be developing an application that replicates the swiping feature present in applications like Inshorts and Flipboard.

Android Nested ViewPager

If you’ve used the above-mentioned applications, you would have realized how easy it is to browse through different contents by just swiping up or down the current page.

The underlying view used to create such features is a ViewPager.

To brief up, ViewPagers use a PagerAdapter that adds pages (generally in the form of Fragment views) to our ViewPager.

By default, ViewPager has a built-in swipe gesture to swipe left or right. There’s an interface named PageTransformer that gets invoked while a scrolling on a ViewPager happens.

The PagerTransform interface contains the following public method that can be overridden and implemented.


void transformPage (View page, float position)
  • page: The current view on which the transformation would be applied.
  • position: Position of the page relative to the current front-and-center position of the pager. 0 is front and center. 1 is one full page position to the right, and -1 is one-page position to the left.

Nested ViewPager is simply a ViewPager wrapped around another ViewPager.

What does this mean?

It means that we have a parent ViewPager in each page of a fragment. The child ViewPager would be hosted inside each of the above fragments.

In the following application, our parent View Pager would we a vertical swiping one. The child View Pager would be the default one that swipes/scrolls horizontally.

We’ll be developing an application that holds a list of Android Tutorials with their descriptions in a Vertical ViewPager. On swiping right, the tutorial would be displayed in a WebView.

Android Nested ViewPager Project

Create a new project in Android Studio and select the Tabbed Activity template from the below screen.

android-studio-kotlin-loading-screen

We will see a MainActivity.java class generated as shown below.


package com.journaldev.swipeviewpagerinshorts;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
    /**
     * The {@link android.support.v4.view.PagerAdapter} that will provide
     * fragments for each of the sections. We use a
     * {@link FragmentPagerAdapter} derivative, which will keep every
     * loaded fragment in memory. If this becomes too memory intensive, it
     * may be best to switch to a
     * {@link android.support.v4.app.FragmentStatePagerAdapter}.
     */
    private SectionsPagerAdapter mSectionsPagerAdapter;
    /**
     * The {@link ViewPager} that will host the section contents.
     */
    private ViewPager mViewPager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        // Create the adapter that will return a fragment for each of the three
        // primary sections of the activity.
        mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
        // Set up the ViewPager with the sections adapter.
        mViewPager = (ViewPager) findViewById(R.id.container);
        mViewPager.setAdapter(mSectionsPagerAdapter);
      FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
      fab.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                      .setAction("Action", null).show();
          }
      });
    }
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();
        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }
    /**
     * A {@link FragmentPagerAdapter} that returns a fragment corresponding to
     * one of the sections/tabs/pages.
     */
    public class SectionsPagerAdapter extends FragmentPagerAdapter {
        public SectionsPagerAdapter(FragmentManager fm) {
            super(fm);
        }
        @Override
        public Fragment getItem(int position) {
            // getItem is called to instantiate the fragment for the given page.
            // Return a PlaceholderFragment (defined as a static inner class below).
            return PlaceholderFragment.newInstance(position + 1);
        }
        @Override
        public int getCount() {
            // Show 3 total pages.
            return 3;
        }
    }
    /**
     * A placeholder fragment containing a simple view.
     */
    public static class PlaceholderFragment extends Fragment {
        /**
         * The fragment argument representing the section number for this
         * fragment.
         */
        private static final String ARG_SECTION_NUMBER = "section_number";
        /**
         * Returns a new instance of this fragment for the given section
         * number.
         */
        public static PlaceholderFragment newInstance(int sectionNumber) {
            PlaceholderFragment fragment = new PlaceholderFragment();
            Bundle args = new Bundle();
            args.putInt(ARG_SECTION_NUMBER, sectionNumber);
            fragment.setArguments(args);
            return fragment;
        }
        public PlaceholderFragment() {
        }
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            View rootView = inflater.inflate(R.layout.fragment_main, container, false);
            TextView textView = (TextView) rootView.findViewById(R.id.section_label);
            textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
            return rootView;
        }
    }
}

Alongside the activity, the class holds the Adapter and Fragment classes as well. This basic template displays a text on each of the pages in a ViewPager. Let’s look at the project structure before we begin with our implementation.

Android Nested ViewPager Project Structure

android-nested-viewpager-project-structure

Add the Internet permission in the AndroidManifest.xml file in the manifest tag.


<uses-permission android:name="android.permission.INTERNET"/>
  • MainActivity – That holds the parent ViewPager, namely the VerticalViewPager. Invokes the ParentViewPagerAdapter that creates instances of ParentFragment
  • VerticalViewPager – Contains a custom implementation of ViewPager that scrolls vertically. The pages hold a ParentFragment view which is supplied by ParentViewPagerAdapter.
  • ParentFragment – It’s present inside the VerticalViewPager. It holds another ViewPager and it’s adapter code is in the file ChildViewPagerAdapter. Fragment’s layout is defined in fragment_parent.xml.
  • ChildViewPagerAdapter – Contains the implementation of nested ViewPager. It holds and supplies ChildFragment‘s to the ParentFragment.
  • ChildFragment – Contains the UI that you’ll see. Layout file : fragment_child.xml
  • DataModel – Contains the data that’s supplied to the ChildFragment.

Android Nested ViewPager, Vertical ViewPager Code

The code for the VerticalViewPager.java class is given below:


package com.journaldev.swipeviewpagerinshorts;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
public class VerticalViewPager extends ViewPager {
    public VerticalViewPager(Context context) {
        super(context);
        init();
    }
    public VerticalViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    private void init() {
        setPageTransformer(true, new VerticalPageTransformer());
        setOverScrollMode(OVER_SCROLL_NEVER);
    }
    private class VerticalPageTransformer implements ViewPager.PageTransformer {
        @Override
        public void transformPage(View view, float position) {
            if (position < -1) { // [-Infinity,-1)
                // This page is way off-screen to the left.
                view.setAlpha(0);
            } else if (position <= 1) { // [-1,1]
                view.setAlpha(1);
                // Counteract the default slide transition
                view.setTranslationX(view.getWidth() * -position);
                //set Y position to swipe in from top
                float yPosition = position * view.getHeight();
                view.setTranslationY(yPosition);
            } else { // (1,+Infinity]
                // This page is way off-screen to the right.
                view.setAlpha(0);
            }
        }
    }
    /**
     * Swaps the X and Y coordinates of your touch event.
     */
    private MotionEvent swapXY(MotionEvent ev) {
        float width = getWidth();
        float height = getHeight();
        float newX = (ev.getY() / height) * width;
        float newY = (ev.getX() / width) * height;
        ev.setLocation(newX, newY);
        return ev;
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev){
        boolean intercepted = super.onInterceptTouchEvent(swapXY(ev));
        swapXY(ev);
        return intercepted;
    }
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return super.onTouchEvent(swapXY(ev));
    }
}

setOverScrollMode(OVER_SCROLL_NEVER) is used to prevent over scrolling.

As we’d discussed before, we create a custom implementation of PageTransformer class where instead of translating the view horizontally using translationX, we do so vertically using translationY. Same is done for motion events when the user swipes on the screen.

The code for DataModel.java is given below.


package com.journaldev.swipeviewpagerinshorts;
public class DataModel {
    public String title, description, url;
    public DataModel(String title, String description, String url) {
        this.title = title;
        this.description = description;
        this.url = url;
    }
}

The code of the activity_main.xml layout file is given below.


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
    android:id="@+id/main_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.journaldev.swipeviewpagerinshorts.VerticalViewPager
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorPrimaryDark" />
</RelativeLayout>

It hosts a VerticalViewPager only, which will in turn host the Parent Fragment.

The code of the MainActivity.java class is given below.


package com.journaldev.swipeviewpagerinshorts;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity implements ParentFragment.ToggleVerticalViewPagerScrolling {
    private ParentViewPagerAdapter verticalPagerAdapter;
    private VerticalViewPager verticalViewPager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ArrayList<DataModel> dataModels = new ArrayList<>();
        dataModels.add(new DataModel("Android Volley Tutorial", getString(R.string.volley_description), getString(R.string.volley_url)));
        dataModels.add(new DataModel("Android Dagger 2", getString(R.string.dagger_description), getString(R.string.dagger_url)));
        dataModels.add(new DataModel("Android Geocoder Reverse Geocoding", getString(R.string.geocoder_description), getString(R.string.geocoder_url)));
        dataModels.add(new DataModel("Android Notification Direct Reply", getString(R.string.notification_description), getString(R.string.notification_url)));
        dataModels.add(new DataModel("RecyclerView Android with Dividers and Contextual Toolbar", getString(R.string.recyclerview_description), getString(R.string.recyclerview_url)));
        verticalPagerAdapter = new ParentViewPagerAdapter(getSupportFragmentManager(), dataModels);
        verticalViewPager = findViewById(R.id.container);
        verticalViewPager.setAdapter(verticalPagerAdapter);
    }
    @Override
    public void trigger(int page) {
        if (page == 1) {
            verticalViewPager.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    return true;
                }
            });
        } else {
            verticalViewPager.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    return false;
                }
            });
        }
    }
}

Things to note:

  • The VerticalViewPager’s adapter, namely ParentViewPagerAdapter is loaded with the data.
  • The trigger method is overridden from an interface defined in the ParentFragment class. It’s invoked by the ParentFragment whenever a page is changed. Its goal is to disable VerticalViewPager from scrolling/swiping when the Nested ViewPager is showing the second page(since the second page holds a WebView) that we’ll be seeing shortly.

Note: To avoid long strings in the class, we’ve defined them in the strings.xml resources file in our project:

android-nested-viewpager-strings-resources

Code for the ParentViewPagerAdapter.java class is given below.


package com.journaldev.swipeviewpagerinshorts;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import java.util.ArrayList;
public class ParentViewPagerAdapter extends FragmentPagerAdapter {
    ArrayList<DataModel> dataModels = new ArrayList<>();
    public ParentViewPagerAdapter(FragmentManager fm, ArrayList<DataModel> dataModels) {
        super(fm);
        this.dataModels = dataModels;
    }
    @Override
    public Fragment getItem(int position) {
        return ParentFragment.newInstance(dataModels.get(position));
    }
    @Override
    public int getCount() {
        return 5;
    }
}

The adapter creates a new ParentFragment class for each page. In total 5 pages for each of the DataModel elements.

The layout for the ParentFragment class is defined in fragment_parent.xml file as shown below.


<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
    android:id="@+id/rl"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimaryDark">
    <android.support.v4.view.ViewPager
        android:id="@+id/nestedViewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</RelativeLayout>

The ParentFragment class holds the Nested ViewPager.


package com.journaldev.swipeviewpagerinshorts;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewPager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class ParentFragment extends Fragment {
    ViewPager nestedViewPager;
    Activity mActivity;
    ToggleVerticalViewPagerScrolling tv;
    public ParentFragment() {
    }
    public static ParentFragment newInstance(DataModel dataModel) {
        ParentFragment fragment = new ParentFragment();
        Bundle args = new Bundle();
        args.putString("title", dataModel.title);
        args.putString("description", dataModel.description);
        args.putString("url", dataModel.url);
        fragment.setArguments(args);
        return fragment;
    }
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_parent, container, false);
        String title = getArguments().getString("title");
        String description = getArguments().getString("description");
        String url = getArguments().getString("url");
        DataModel model = new DataModel(title, description, url);
        nestedViewPager = rootView.findViewById(R.id.nestedViewPager);
        nestedViewPager.setAdapter(new ChildViewPagerAdapter(getChildFragmentManager(), model));
        nestedViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                tv.trigger(position);
            }
            @Override
            public void onPageSelected(int position) {
                Log.d("API123", "onPageSelected " + position);
            }
            @Override
            public void onPageScrollStateChanged(int state) {
            }
        });
        return rootView;
    }
    public interface ToggleVerticalViewPagerScrolling {
        void trigger(int page);
    }
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (context instanceof Activity) {
            mActivity = (Activity) context;
        }
        try {
            tv = (ToggleVerticalViewPagerScrolling) mActivity;
        } catch (ClassCastException e) {
            throw new ClassCastException("Error in retrieving data. Please try again");
        }
    }
}

Things to note:

  • Each ParentFragment.java class holds a Nested ViewPager.
  • The ChildViewPagerAdapter creates a nested fragment (ChildFragment) instance for each page.
  • The Nested ViewPager would hold two pages(ChildFragment instances) only. The first would contain the title and description of the tutorial. The second page would show the tutorial in a WebView.
  • addOnPageChangeListener callback is used to determine the current page index.
  • interface ToggleVerticalViewPagerScrolling contains the method trigger. The trigger method passes the relevant page index to the MainActivity. Read communicating data from fragments
  • If the page index is 2 i.e. the WebView UI, the VerticalViewPager swiping is disabled.

The code for the ChildViewPagerAdapter.java class is given below.


package com.journaldev.swipeviewpagerinshorts;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
public class ChildViewPagerAdapter extends FragmentPagerAdapter {
    DataModel model;
    public ChildViewPagerAdapter(FragmentManager fm, DataModel model) {
        super(fm);
        this.model = model;
    }
    @Override
    public int getCount() {
        return 2;
    }
    @Override
    public Fragment getItem(int position) {
        switch (position) {
            case 0:
                return ChildFragment.newInstance(model, false);
            case 1:
                return ChildFragment.newInstance(model, true);
            default:
                return ChildFragment.newInstance(model, true);
        }
    }
    @Override
    public CharSequence getPageTitle(int position) {
        return "Child Fragment " + position;
    }
}

The code for the layout fragment_child.xml is given below.


<android.support.v7.widget.CardView xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:app="https://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginBottom="@dimen/activity_vertical_margin"
    android:layout_marginEnd="@dimen/activity_horizontal_margin"
    android:layout_marginStart="@dimen/activity_horizontal_margin"
    android:layout_marginTop="@dimen/activity_vertical_margin"
    app:cardCornerRadius="8dp"
    app:cardElevation="8dp"
    app:cardPreventCornerOverlap="true"
    app:cardUseCompatPadding="true">
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <WebView
            android:id="@+id/webView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:visibility="gone" />
        <RelativeLayout
            android:id="@+id/rl"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:background="#262626">
            <TextView
                android:id="@+id/txtTitle"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:gravity="center"
                android:textColor="#FFF"
                android:textSize="26sp" />
        </RelativeLayout>
        <TextView
            android:id="@+id/txtDescription"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/rl"
            android:layout_centerHorizontal="true"
            android:layout_marginBottom="@dimen/activity_vertical_margin"
            android:layout_marginEnd="@dimen/activity_horizontal_margin"
            android:layout_marginStart="@dimen/activity_horizontal_margin"
            android:layout_marginTop="@dimen/activity_vertical_margin"
            android:textSize="18sp" />
        <Button
            android:id="@+id/button"
            android:background="#801f2124"
            android:text="TAP/SLIDE RIGHT TO VIEW TUTORIAL"
            android:textColor="#FFF"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true" />
    </RelativeLayout>
</android.support.v7.widget.CardView>

The code for the ChildFragment.java class is given below.


package com.journaldev.swipeviewpagerinshorts;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.widget.Button;
import android.widget.RelativeLayout;
import android.widget.TextView;
public class ChildFragment extends Fragment {
    public ChildFragment() {
    }
    public static ChildFragment newInstance(DataModel dataModel, boolean isWebView) {
        ChildFragment fragment = new ChildFragment();
        Bundle args = new Bundle();
        args.putString("title", dataModel.title);
        args.putString("description", dataModel.description);
        args.putString("url", dataModel.url);
        args.putBoolean("isWebView", isWebView);
        fragment.setArguments(args);
        return fragment;
    }
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_child, container, false);
        RelativeLayout rl = rootView.findViewById(R.id.rl);
        WebView webView = rootView.findViewById(R.id.webView);
        TextView txtTitle = rootView.findViewById(R.id.txtTitle);
        Button button = rootView.findViewById(R.id.button);
        TextView txtDescription = rootView.findViewById(R.id.txtDescription);
        boolean isWebView = getArguments().getBoolean("isWebView");
        if (isWebView) {
            webView.setVisibility(View.VISIBLE);
            rl.setVisibility(View.GONE);
            button.setVisibility(View.GONE);
            txtDescription.setVisibility(View.GONE);
            webView.loadUrl(getArguments().getString("url"));
        } else {
            webView.setVisibility(View.GONE);
            rl.setVisibility(View.VISIBLE);
            txtDescription.setVisibility(View.VISIBLE);
            button.setVisibility(View.VISIBLE);
            txtTitle.setText(getArguments().getString("title"));
            txtDescription.setText(getArguments().getString("description"));
        }
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ParentFragment parentFrag = ((ParentFragment) ChildFragment.this.getParentFragment());
                parentFrag.nestedViewPager.setCurrentItem(1);
            }
        });
        return rootView;
    }
}

The isWebView parameter from newInstance method determines whether the WebView is shown/hidden. Button click would directly take the user to the WebView page.

Changing ViewPager page dynamically.

To change the ViewPager page we call the setCurrentItem() method on the ViewPager instance.

Don’t forget to add the CardView dependency to the build.gradle.

Android Vertical ViewPager App Output

The output of the above application in action.
android nested viewpager output

Adding Animation to Swipe Effect

Let’s animate the VerticalViewPager scrolling by creating a different PageTransformer class.


private class VerticalPageTransformerAnimate implements ViewPager.PageTransformer {
        private static final float MIN_SCALE = 0.75f;
        @Override
        public void transformPage(View view, float position) {
            int pageWidth = view.getWidth();
            int pageHeight = view.getHeight();
            float alpha = 0;
            if (0 <= position && position <= 1) {
                alpha = 1 - position;
            } else if (-1 < position && position < 0) {
                float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));
                float verticalMargin = pageHeight * (1 - scaleFactor) / 2;
                float horizontalMargin = pageWidth * (1 - scaleFactor) / 2;
                if (position < 0) {
                    view.setTranslationX(horizontalMargin - verticalMargin / 2);
                } else {
                    view.setTranslationX(-horizontalMargin + verticalMargin / 2);
                }
                view.setScaleX(scaleFactor);
                view.setScaleY(scaleFactor);
                alpha = position + 1;
            }
            view.setAlpha(alpha);
            view.setTranslationX(view.getWidth() * -position);
            float yPosition = position * view.getHeight();
            view.setTranslationY(yPosition);
        }
    }

Initialise in the init method of the VerticalViewPager class in the following way.


setPageTransformer(true, new VerticalPageTransformerAnimate());

The new output of the application is:
android vertical viewpager animation example output

This brings an end to android vertical ViewPager tutorial. Since the ViewPagers are swipeable in all four directions we’ve named the project as SwipeViewPagerInshorts. You can download it from the link below.

By admin

Leave a Reply

%d bloggers like this: