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
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):
getCount()
should return the number of views available, i.e., number of pages to be displayed/created in the ViewPager
.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.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.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.
PagerAdapter
is base class for both FragmentPagerAdapter
and FragmentStatePagerAdapter
.If you have to show custom views (not Fragments) then subclass it and override its:
instantiateItem(),
destroyItem(),
getCount(),
isViewFromObject()``.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.getCount()
returns the number of items which will be shown in the ViewPager
.isViewFromObject()
method checks whether the Object returned from instantiateItem()
method is linked to the View
supplied here.FragmentPagerAdapter
FragmentPagerAdapter
we implement only getItem()
and getCount()
methods in order to get a working adapter.getItem()
is called by the instantiateItem()
internally in order to create a new fragment.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.onAttach()
, onCreate()
, onCreateView()
, onViewCreated()
, onActivityCreated()
, onViewStateRestored()
, onStart()
, and onResume()
methods will be called sequentially.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 Fragment
s 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 Fragment
s 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.