time

How to create slider with ViewPager in Android

How to create slider with ViewPager in Android

One common UI requirement is sliding between multiple screens. For example, a photo slideshow. Android provides a UI control for creating sliding screens called the ViewPager.

ViewPager is a modified implementation of the AdapterView pattern that the framework uses for widgets such as ListView and GridView. It requires its own adapter implementation as a subclass of PagerAdapter, but it is conceptually very similar to the patterns used in BaseAdapter and ListAdapter. It does not inherently implement recycling of the components being paged, but it does provide callbacks to create and destroy the items on the fly so that only a fixed number of content views are in memory at a given time.

ViewPager works by keeping track of a key object for each item alongside a view to display for that object; this keeps the separation between the adapter items and their views that developers are used to with AdapterView. However, the implementation is a bit different. With AdapterView, the adapter’s getView() method is called to construct and return the view to display for that item. With ViewPager, the callback’s instantiateItem() and destroyItem() will be called when a new view needs to be created, or when one has scrolled outside the bounds of the pager’s limit and should be removed; the number of items that any ViewPager will keep hold of is set by the setOffscreenPageLimit() method.

This article will explain using ViewPager to create a sliding screen UI in your Android apps.

Create a new Blank Activity project and begin by adding a ViewPager to the activity. Change activity_main.xml to the following:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/holo_blue_light"
    android:padding="5dp"
    android:orientation="vertical">

    <android.support.v4.view.ViewPager
        android:id="@+id/vpPager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="0.85">
    </android.support.v4.view.ViewPager>

    <me.relex.circleindicator.CircleIndicator
        android:id="@+id/indicator"
        android:layout_width="match_parent"
        android:layout_height="48dp" />
</LinearLayout>

In the above layout, there is a LinearLayout containing a ViewPager with an id of vpPage. There is also a view called CircleIndicator which indicates the dots at the bottom of the screen depending on the number of pages in the ViewPager.

As indicator you can use any of following

Next step is define Adapter wich will supply items for our ViewPager. There are three ways PagerAdapter, FragmentPagerAdapter and FragmentStatePagerAdapter.

Let's start with FragmentPagerAdapter. You need to define each screen in the ViewPager as a fragment. Each screen could be an instance of different Fragment classes, or different instances of the same fragment class with varying content. This app will contain three screens with ImageView and TextView.

The page_item.xml layout is as follow:

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

    <ImageView
        android:id="@+id/ivIcon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        />

    <TextView
        android:id="@+id/tvTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@android:color/white"
        android:layout_gravity="center_horizontal"
        android:textSize="20dp"/>
</LinearLayout>

Result

android_slider_result.png

In following code PageFragment, has a getInstance method which takes the title and the image resource id displayed in the fragment. The fragment inflates the page_item.xml layout and sets the title and the image resource id.

public class PageFragment extends Fragment {
    private String title;
    private int image;

    public static PageFragment newInstance(String title, int resImage) {
        PageFragment fragment = new PageFragment();
        Bundle args = new Bundle();
        args.putInt("image", resImage);
        args.putString("title", title);
        fragment.setArguments(args);
        return fragment;
    }


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        image = getArguments().getInt("image", 0);
        title = getArguments().getString("title");
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.page_item, container, false);
        TextView tvTitle = (TextView) view.findViewById(R.id.tvTitle);
        tvTitle.setText(title);

        ImageView ivIcon = (ImageView) view.findViewById(R.id.ivIcon);
        ivIcon.setImageResource(image);
        return view;
    }
}

Now you’ve created the fragments you need a PagerAdapter which you will set on the ViewPager. Although you can directly extend from PagerAdapter, Android provides another base class FragmentPagerAdapter which implements basic functionality to show the fragments as pages in the PagerAdapter.

Extend the adapter from FragmentPagerAdapter by adding this class to MainActivity.java.

public class PageAdapter extends FragmentPagerAdapter {
    private static int NUM_ITEMS = 3;

    public PageAdapter(FragmentManager fragmentManager) {
        super(fragmentManager);
    }

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

    @Override
    public Fragment getItem(int position) {
        switch (position) {
            case 0:
                return PageFragment.newInstance("EMail", android.R.drawable.ic_dialog_email);
            case 1:
                return PageFragment.newInstance("Alert", android.R.drawable.ic_dialog_alert);
            case 2:
                return PageFragment.newInstance("Dialer", android.R.drawable.ic_dialog_dialer);
            default:
                return null;
        }
    }
}

The PageAdapter class extends from FragmentPagerAdapter and overrides the getCount method which returns how many pages the adapter will display. In this example, you want to show three pages, and it returns three. The getItem method returns the instance of the fragment to display on the screen.

Instead of FragmentPagerAdapter we can use basic PagerAdapter.

class CustomPageAdapter extends PagerAdapter {
    Context context;
    LayoutInflater layoutInflater;

    int[] resources = {R.drawable.image1, R.drawable.image2, R.drawable.image3};

    public CustomPagerAdapter(Context context) {
        context = context;
        layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    @Override
    public int getCount() {
        return resources.length;
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view == ((LinearLayout) object);
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        View itemView = layoutInflater.inflate(R.layout.page_item, container, false);

        ImageView ivIcon = (ImageView) itemView.findViewById(R.id.ivIcon);
        ivIcon.setImageResource(resources[position]);

        container.addView(itemView);

        return itemView;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        container.removeView((LinearLayout) object);
    }
}

Let's discuss the four methods that we've overridden (mandatory):

  • Method getCount() should return the number of views available, i.e., number of pages to be displayed/created in the ViewPager.
  • Method instantiateItem() should create the page for the given position passed to it as an argument. In our case, we inflate() our layout resource to create the hierarchy of view objects and then set resource for the ImageView in it. Finally, the inflated view is added to the container (which should be the ViewPager) and return it as well.
  • Method destroyItem() removes the page from the container for the given position. We simply removed object using removeView() but could’ve also used removeViewAt() by passing it the position.
  • Method isViewFromObject() checks whether the View passed to it (representing the page) is associated with that key or not. The object returned by instantiateItem() is a key/identifier. It is required by a PagerAdapter to function properly. For our example, the implementation of this method is really simple, we just compare the two instances and return the evaluated boolean.

It is important to understand that as the user navigates to a particular page, the one next to it is generated by calling instantiateItem() while the one before the previous one gets destroyed by calling destroyItem(). This caching limit (destruction and rebuilding limit) can be specified by the setOffscreenPageLimit() method on the ViewPager object which is set to 1 by default. Increasing this value to a higher number leads to a smoother navigation as far as the animations and interactions are concerned as everything is retained in memory but then it can also cause a memory overhead affecting the app’s performance. So you’ve to find the perfect balance in your case.

Once you have created the PageAdapter you need to set the instance of it on the ViewPager in the onCreate function as below:

public class MainActivity extends AppCompatActivity {

    PagerAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ViewPager pager = (ViewPager) findViewById(R.id.vpPager);
        adapter = new PageAdapter(getSupportFragmentManager());
        pager.setAdapter(adapter);

        CircleIndicator indicator = (CircleIndicator) findViewById(R.id.indicator);
        indicator.setViewPager(pager);
    }
}

You can implement different animations on the page slide by setting a ViewPager.PageTransformer. To implement this you have to override the transformPage method. In this function you provide custom animations based on the position on the screen, there are a lot of transforms available as open source libraries which you can use, for example the ToxicBakery.ViewPagerTransforms.

To use a custom animation, add it to your dependencies in build.gradle (Module: App) as follows:

dependencies {
    ...
    compile 'com.ToxicBakery.viewpager.transforms:view-pager-transforms:1.2.32@aar'
}

Now you can add the PageTransformer on the ViewPager in the onCreate method as shown:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    ViewPager pager = (ViewPager) findViewById(R.id.vpPager);
    adapter = new PageAdapter(getSupportFragmentManager());
    vpPager.setAdapter(adapter);

    CircleIndicator indicator = (CircleIndicator) findViewById(R.id.indicator);
    indicator.setViewPager(pager);

    pager.setPageTransformer(true, new RotateUpTransformer());
}

Also you can use a collection of view pager transformers android-viewpager-transformers.

Dynamically add slides to ViewPager

First of all, let's dive into Adapter and examine kinds of it. Adapter supplies items for ViewPager. For ViewPager we can use PagerAdapter which is base class for FragmentPagerAdapter and FragmentStatePagerAdapter

There are two subclass of PagerAdapters:

  • FragmentPagerAdapter. The purpose for using this type of fragment adapter is to keep the whole fragment in memory. Its view hierarchy may be destroyed but the fragment may still remain in memory. Hence it is advised to use this Android FragmentPagerAdapter only when there are low number of static fragments, since all fragments would be kept in the memory and this would increase the memory heap. Which could result a slow running app.
  • FragmentStatePagerAdapter. The main functionality for which FragmentStatePagerAdapter is used, to accommodate a large number of fragments in ViewPager. As this adapter destroys the fragment when it is not visible to the user and only savedInstanceState of the fragment is kept for further use. This way a low amount of memory is used and a better performance is delivered in case of dynamic fragments.

Second, let's look at each Adapter in details.

PagerAdapter.

  1. PagerAdapter is base class for both FragmentPagerAdapter and FragmentStatePagerAdapter.
  2. If you have to show custom views (not Fragments) then subclass it and override its:instantiateItem(),destroyItem(),getCount(),isViewFromObject()``.
  3. PagerAdapter keeps maximum three views in memory, one which is currently visible, one which is left and one is right of the visible item. While scrolling, the pages which goes out of the screen will be destroyed in destroyItem() method.
  4. getCount() returns the number of items which will be shown in the ViewPager.
  5. isViewFromObject() method checks whether the Object returned from instantiateItem() method is linked to the View supplied here.

FragmentPagerAdapter

  1. In FragmentPagerAdapter we implement only getItem() and getCount() methods in order to get a working adapter.
  2. getItem() is called by the instantiateItem() internally in order to create a new fragment.
  3. In FragmentPagerAdapter, once a fragment has been created, it will never be destroyed throughout the life of the adapter. The fragments will be held by the FragmentManager and will be reused again whenever we will require a new fragment to show. Fragments needed again will be fetched from the FragmentManager and its view hierarchy will be created again by going through onCreateView(), onViewCreated(), onActivityCreated(), onViewStateRestored(), onStart() and onResume() methods.
  4. If the fragment is being created first time its onAttach(), onCreate(), onCreateView(), onViewCreated(), onActivityCreated(), onViewStateRestored(), onStart(), and onResume() methods will be called sequentially.
  5. If the fragment is going off the screen its onPause(), onStop(), onDestroyView() methods will be called in order. Note that the fragments onDestroy() and onDetach() methods will never be called when the fragments are going off the screen, means fragments are not being destroyed once created and will be held by the FragmentManager.

If the number of fragments are large, it will take a lot of memory if we use FragmentPagerAdapter because it never destroys the Fragments once created. It only destroys fragments’s view hierarchy and keeps its state internally. To overcome this shortcoming we have FragmentStatePagerAdapter which we are going to discuss here.

FragmentStatePagerAdapter

FragmentStatePagerAdapter differs from the FragmentPagerAdapter only in one way that it destroys the fragments which are going off the screen. So Fragments are created, attached, destroyed and detached keeping the memory uses low. It holds maximum three fragments and keep destroying the off screen fragments. It is suitable in the situation where the number of fragments are large. In every other aspect it is just like the FragmentPagerAdapter.

Let's write simple example how to dynamically add slides to ViewPager. Below is activity_main.xml.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/holo_blue_light"
    android:padding="5dp"
    android:orientation="vertical">

    <android.support.v4.view.ViewPager
        android:id="@+id/vpPager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="0.85">
    </android.support.v4.view.ViewPager>
</LinearLayout>

Below is MainActivity.java.

public class MainActivity extends FragmentActivity {
    PageAdapter adapter;
    ViewPager pager;
    int previousPosition = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        List<Fragment> items = new ArrayList<>();
        items.add(PageFragment.newInstance("Page 1"));
        items.add(PageFragment.newInstance("Page 2"));
        items.add(PageFragment.newInstance("Page 3"));

        adapter = new PageAdapter(getSupportFragmentManager(), items);
        pager = (ViewPager) findViewById(R.id.vpPager);
        pager.setAdapter(adapter);
        pager.setOffscreenPageLimit(3);

        pager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}

            @Override
            public void onPageSelected(int position) {
                boolean isLeftSwipe = previousPosition  > position;

                if (isLeftSwipe) {
//                    Toast.makeText(FourthActivity.this, "left", Toast.LENGTH_SHORT).show();
                } else {
//                    Toast.makeText(FourthActivity.this, "right", Toast.LENGTH_SHORT).show();

                    if (adapter.getCount() < 7) {
                        adapter.addItem(PageFragment.newInstance("Page " + 
                             String.valueOf(adapter.getCount() +  1)));
                    }
                }
                previousPosition = position;
                Toast.makeText(FourthActivity.this, (position + 1) + "/" + 
                    adapter.getCount(), Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onPageScrollStateChanged(int state) {}
        });

    }

    @Override
    public void onBackPressed() {
        if (pager.getCurrentItem() == 0) {
            // If the user is currently looking at the first step, allow the system to handle the
            // Back button. This calls finish() on this activity and pops the back stack.
            super.onBackPressed();
        } else {
            // Otherwise, select the previous step.
            pager.setCurrentItem(pager.getCurrentItem() - 1);
        }
    }
}

setOffscreenPageLimit sets the number of pages that should be retained to either side of the current page in the view hierarchy in an idle state. Pages beyond this limit will be recreated from the adapter when needed.

Below is PageAdapter.java.

public class PageAdapter extends FragmentPagerAdapter {
    List<Fragment> items = new ArrayList<>();

    public PageAdapter(FragmentManager fragmentManager, List<Fragment> items) {
        super(fragmentManager);
        this.items = items;
    }

    @Override
    public int getCount() {
        return items.size();
    }

    @Override
    public Fragment getItem(int position) {
        return items.get(position);
    }


    public void addItem(PageFragment item) {
        items.add(item);
        notifyDataSetChanged();
    }

    public void removeItem(int index) {
        items.remove(index);
        notifyDataSetChanged();
    }
}

Below is PageFragment.java.

public class PageFragment extends Fragment {
    private String title;

    public static PageFragment newInstance(String title) {
        PageFragment fragment = new PageFragment();
        Bundle args = new Bundle();
        args.putString("title", title);
        fragment.setArguments(args);
        return fragment;
    }


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        title = getArguments().getString("title");
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        View view = inflater.inflate(R.layout.page_item, container, false);
        TextView tvTitle = (TextView) view.findViewById(R.id.tvTitle);
        tvTitle.setText(title);

        return view;
    }
}

Below is page_item.xml layout for PageFragment.

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

    <TextView
        android:id="@+id/tvTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@android:color/black"
        android:layout_gravity="center_horizontal"
        android:textSize="20dp"/>
</LinearLayout>

How to create vertical ViewPager

You can use a ViewPager.PageTransformer to give the illusion of a vertical ViewPager. To achieve scrolling with a vertical instead of a horizontal drag you will have to override ViewPager's default touch events and swap the coordinates of MotionEvents prior to handling them, e.g.:

Define VerticalViewPager in VerticalViewPager.java.

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) {
                view.setAlpha(0);
            } else if (position <= 1) {
                view.setAlpha(1);
                view.setTranslationX(view.getWidth() * -position);
                float yPosition = position * view.getHeight();
                view.setTranslationY(yPosition);
            } else { 
                view.setAlpha(0);
            }
        }
    }

    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));
    }
}

Instead of VerticalPageTransformer you can use any other animation.

Below is MainActivity.java.

public class MainActivity extends AppCompatActivity {
    CustomPageAdapter adapter;
    VerticalViewPager pager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fifth);

        adapter = new CustomPageAdapter(this);
        pager = (VerticalViewPager) view.findViewById(R.id.vpPager);
        pager.setAdapter(adapter);
    }
}

Simple activity_main.xml layout for MainActivity.java.

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

    <me.proft.projectA.VerticalViewPager
        android:id="@+id/vpPager"
        android:layout_width="match_parent"
        android:layout_height="200dp">
    </me.proft.projectA.VerticalViewPager>

    <TextView
        android:text="Lorem ipsum dolor sit amet."
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>        

How to create circular ViewPager (endless scrolling)

I'm going to use CircularViewPager. To use this library you should follow tips from following list.

First, add dependency to your buid.gradle

compile 'com.github.tobiasbuchholz:circularviewpager:1.0.0'

Second, create an adapter for your ViewPager and let it extend the BaseCircularViewPagerAdapter and implement it's methods.

public class SliderAdapter extends BaseCircularViewPagerAdapter<Integer> {
    private final Context context;

    public SliderAdapter(Context context, FragmentManager fragmentManager, List<Integer> images) {
        super(fragmentManager, images);
        this.context = context;
    }

    @Override
    protected Fragment getFragmentForItem(Integer imageID) {
        return SliderItemFragment.newInstance(imageID);
    }
}

Third, define Fragment for item

public class SliderItemFragment extends Fragment {
    private static final String EXTRA_IMAGE_ID = "EXTRA_IMAGE_ID";
    private int imageID;

    public static SliderItemFragment newInstance(int imageID) {
        Bundle args = new Bundle();
        args.putInt(EXTRA_IMAGE_ID, imageID);

        SliderItemFragment fragment = new SliderItemFragment();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Bundle arguments = getArguments();
        if(arguments != null) {
            imageID = arguments.getInt(EXTRA_IMAGE_ID);
        }
    }

    @Override
    public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
        final View view = inflater.inflate(R.layout.slider_item, container, false);
        ImageView iv = (ImageView) view.findViewById(R.id.ivPhoto);
        iv.setImageResource(imageID);
        return view;
    }
}

and it's layout in layout slider_item.xml

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

    <ImageView
        android:id="@+id/ivPhoto"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

Last, initialize your ViewPager and set the adapter to it. Also create a CircularViewPagerHandler and set it as ViewPager.OnPageChangeListener to the ViewPager.

public class SliderActivity extends AppCompatActivity {
    ViewPager pager;
    SliderAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_slider);
        pager = (ViewPager) findViewById(R.id.vpPager);

        List<Integer> images = Arrays.asList(R.drawable.img1, R.drawable.img2, R.drawable.img3, R.drawable.img4);

        adapter = new SliderAdapter(this, getSupportFragmentManager(), images);

        pager.setAdapter(adapter);
        pager.addOnPageChangeListener(new CircularViewPagerHandler(pager));
        pager.setCurrentItem(0);
    }
}

The layout for SliderActivity is following

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

    <android.support.v4.view.ViewPager
        android:id="@+id/vpPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:overScrollMode="never">
    </android.support.v4.view.ViewPager>

</LinearLayout>

That's it.

Also, check cyclicview it is circual scrolling view implementation for Android instead ViewPager hacks.

comments powered by Disqus