News & Blog

Android - Viewpager with validation

27.11.2015 | Entwicklerblog

Introduction

In Android development, a horizontal slider which enables users to swipe between screens, is a common method to display tutorials, registration forms which consist of several steps, and more. This is usually implemented by using a viewpager. This viewpager doesn't have an inbuilt validation functionality, which could be used, for example, to make sure that the user enters all necessary registration data before sliding to the next screen. The easiest way could be to simply disable the swiping functionality altogether, add buttons and allowing to go to the next or previous page by using buttons only. However this is not user friendly, so a different approach was applied. One way to add the missing validation is to use a Gesture Detector together with a custom implementation of the viewpager, which has the standard viewpager as its base class.

The idea

Usually the way it works when only using the standard viewpager is that when the user makes a swiping gesture on the screen, the next view, which was defined in the viewpager, is shown. It is possible to attach a SimpleOnPageChangeListener to the viewpager which has the methods onPageScrollStateChanged(int state), onPageScrolled(int position, float positionOffset, int positionOffsetPixels) and onPageSelected(int position), which are useful to handle page changes, but the validation should happen before any kind of scrolling or page changing is happening.

Since the viewpager doesn't have a validation built-in like "only scroll to next page if condition_1 is fulfilled", we need a different approach. This approach makes use of the gesture listener, which is attached to the viewpager, and which will handle the validation and based on the result of this validation, move to the next or previous page programmatically. The code sample below explains in detail what needs to be done.

Implementation

First, we need create a FragmentPagerAdapter which basically tells the viewpager what to display on which position. The following implementation allows to use any Fragment with any kind of layout. Here an example how it can look like:

public static class SectionsPagerAdapter extends FragmentPagerAdapter {
static final SparseArray<Fragment> registeredFragments = new SparseArray();

public SectionsPagerAdapter(FragmentManager fm, Context context) {
super(fm);
this.context = context;
}
public int getNumberOfFragments(){
return numberOfPages;
}

@Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return FragmentOne.newInstance("Title 1", "Description 1");
case 1:
return FragmentTwo.newInstance();
case 2:
return FragmentThree.newInstance();
}
}

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

@Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment fragment = super.instantiateItem(container,
position);
registeredFragments.put(position, fragment);
return fragment;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
registeredFragments.remove(position);
super.destroyItem(container, position, object);
}

public Fragment getRegisteredFragment(int position) {
return registeredFragments.get(position);
}
}

Within the Fragment named "FragmentOne" the newInstance-method can look something like this:

public static FragmentOne newInstance() {
Commissioning_Action_Fragment fragment = new FragmentOne();
fragment.title = title;
fragment.description = description;

return fragment;
}

The variables "title" and "descriptions" are local variables and can be filled in the getItem-method of the SectionsPagerAdapter:

    return FragmentOne.newInstance("Title 1", "Description 1");

This is nothing special and doesn't play any role in allowing or disallowing the user to swipe to a different screen, but is required by the viewpager to work at all.

Now we need a class which overrides the standard viewpager:

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;

public class CustomViewPager extends ViewPager {

private GestureDetector gestureDetector;
private View view;

public CustomViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}

public void setGestureDetector(GestureDetector gestureDetector) {
this.gestureDetector = gestureDetector;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
if (this.gestureDetector != null)
this.gestureDetector.onTouchEvent(event);
return true;
}

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
this.gestureDetector.onTouchEvent(event);
return super.onInterceptTouchEvent(event);
}
}

What we do here is we add a GestureDetector instance, which will be transferred when creating a CustomViewPager-object in the activity or container. The methods onTouchEvent(MotionEvent event) and onInterceptTouchEvent(MotionEvent event) are called when the user touches the screen. The event in those methods is then passed to the onTouchEvent-metod of the GestureDetector, where it will be handled. This is the code for the GestureDetector-listener:

final GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {
private final int minDistance = 20;

@Override
public boolean onDown(MotionEvent event) {
canScroll = true;
return true;
}

@Override
public void onShowPress(MotionEvent motionEvent) {

}

@Override
public boolean onSingleTapUp(MotionEvent motionEvent) {
return true;
}

@Override
public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float distanceX, float distanceY) {
if(motionEvent1.getActionMasked() == MotionEvent.ACTION_MOVE){
if((motionEvent.getX() > motionEvent1.getX()) && (distanceX > minDistance)){
if(canScroll){
canScroll = false;
handleScrollToNextPage(getCurrentItemPosition());
}
}
if((motionEvent.getX() < motionEvent1.getX()) && (distanceX < -minDistance)){
if(canScroll){
canScroll = false;
handleScrollToPreviousPage(getCurrentItemPosition());
}
}
}
return true;
}

@Override
public void onLongPress(MotionEvent motionEvent) {

}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
};

The logic is pretty simple: In the onScroll-method we check the direction of the swipe, and either call "handleScrollToPreviousPage(getCurrentItemPosition)" or "handleScrollToNextPage(getCurrentItemPosition)" (it is also possible to use the onFling-method, but in our case the fling was not always recognized, this is why we used onScroll()). These methods examine if the user is allowed to swipe (by checking if all required fields are entered, for example) and then calls:

if(currentItemPosition < numberOfPages){
mViewPager.setCurrentItem(currentItemPosition + 1, true);
}

Now that we have all required classes, we can put them to use. In the fragment or activity we want the viewpager to be displayed (in this case it is used within a Fragment, thus using getChildFragmentManager instead of getFragmentManager()), we instantiate the objects and pass parameters like this:

GestureDetector gestureDetector = new GestureDetector(getActivity(), onGestureListener);

mSectionsPagerAdapter = new CustomFragmentPagerAdapter(getChildFragmentManager(), getActivity(), this);
mViewPager = (CustomViewPager) view.findViewById(R.id.viewpager);
mViewPager.setAdapter(sectionsPagerAdapter);
mViewPager.setGestureDetector(gestureDetector);

After this, we are done and can test the result!