Differentiating between user scroll and programatic page change in ViewPager
Asked Answered
G

4

39

I have a android.support.v4.view.ViewPager in my application and I would like to differentiate between a programmatically-initiated smooth scroll and a user-initiated touch scroll.

I've had a look at ViewPager.OnPageChangeListener and I believe that the answer may lie in there but I'm just not sure how.

Garrek answered 23/7, 2013 at 20:2 Comment(0)
G
100

OK, so it turns out that I was right about the answer lying in ViewPager.onPageChangeListener. In particular it lies in using onPageScrollStateChanged(int state). Essentially there are three states that a page in a ViewPager can be in:

  1. Dragging: Indicates that the pager is currently being dragged by the user.
  2. Idle: Indicates that the pager is in an idle, settled state.
  3. Settling: Indicates that the pager is in the process of settling to a final position.

So the dragging state only occurs when the current page is being physically dragged by the user. Thus when the user has swiped a page the states occur in the following order: Dragging -> Settling -> Idle. Now, the onPageSelected(int position) method is called between the "Settling" and "Idle" states. Thus, in order to determine whether or not a page change was caused by a user scroll one just needs to check that the previous state was "dragging" and that the current state is "Settling". You can then keep a boolean variable to track whether or not the page change was user initiated or not and check it in your onPageSelected(int position) method.

Here is my onPageScrollStateChanged method

public void onPageScrollStateChanged(int state) 
{
    if (previousState == ViewPager.SCROLL_STATE_DRAGGING
            && state == ViewPager.SCROLL_STATE_SETTLING)
        userScrollChange = true;

    else if (previousState == ViewPager.SCROLL_STATE_SETTLING
            && state == ViewPager.SCROLL_STATE_IDLE)
        userScrollChange = false;

    previousState = state;
}

The if and else if statements need not be so explicit but I did so for clarity.

Garrek answered 23/7, 2013 at 20:53 Comment(3)
Still a good answer in 2016. In fact I'm going to make this into an abstract base class with a wasUserScroll() method for all to use. :)Trench
what initial value should I set for previousState variable?Copyreader
If the user is dragging and programmatically invoke setCurrentItem, userScrollChange will be true.Upmost
C
7

I have based myself on the answer mark as correct and in the comments below.

First I analyze how the complete listener behave:

USER
onPageScrollStateChanged:        1             SCROLL_STATE_DRAGGING
onPageScrollStateChanged:        2             SCROLL_STATE_SETTLING
onPageSelected:              SELECTION     
onPageScrollStateChanged:        0             SCROLL_STATE_IDLE

PROGRAMATIC
onPageScrollStateChanged:        2             SCROLL_STATE_SETTLING
onPageSelected:              SELECTION
onPageScrollStateChanged:        0             SCROLL_STATE_IDLE  

Findings:

  • As you can see in both cases the events end when the onPageScrollStateChanged move to SCROLL_STATE_IDLE, this means idle is the end of the cycle

  • The user event is SCROLL_STATE_DRAGGING and then SCROLL_STATE_SETTLING which are 2 states different than only 1 state for the programmatic event SCROLL_STATE_SETTLING

  • onPageSelected happens before the end of the cycle, but after we are able to determinate if the change was triggered by the user or programmatically, so whatever happened before will tell us in that point if it was the user or not

Solution:

So I use a List<Integer> that is reset every time the cycle ends, and to be able to know if the user triggered the event in the onPageSelected method I check the size of the List. If the size is 2 that means the user scroll the pager.

abstract class PagerListenerActivity extends AppCompatActivity implements ViewPager.OnPageChangeListener {

    private static final int USER_SCROLL = 2;
    private List<Integer> validator = new ArrayList<>();

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    }

    @Override
    public void onPageSelected(int position) {
        if (validator.size() == USER_SCROLL) {
            userScroll(position);
        }
    }

    @Override
    public void onPageScrollStateChanged(int state) {
        validator.add(state);
        if (ViewPager.SCROLL_STATE_IDLE == state) {
            validator.clear();
        }

    }

    protected abstract void userScroll(int position);
}

Now, this class can conveniently be inherited by another that need it.

Crofton answered 8/12, 2017 at 19:25 Comment(0)
P
4

You are right about using ViewPager.OnPageChangeListener:

@Override
public void onPageSelected(int arg0) {
    // programmatically-initiated                           
}

@Override
public void onPageScrolled(int arg0, float arg1, int arg2) {

}

@Override
public void onPageScrollStateChanged(int arg0) {
    // user-initiated touch scroll      
}

Alternatively, you can use boolean flags to differentiate between programmatically-initiated smooth scroll and a user-initiated touch scroll. For example, if you use setCurrentItem(int item) to programmatically change the page, try:

boolean progChange = false;

....
....
....

progChange = true;
setCurrentItem(somePageId);     // Set progChange = true every time

....
....
....

Inside your ViewPager.OnPageChangeListener:

@Override
public void onPageSelected(int arg0) {

}

@Override
public void onPageScrolled(int arg0, float arg1, int arg2) {
    if (progChange) {
        // programmatically-initiated
    } else {
        // user-initiated touch scroll
    }

    // Set progChange to false;
    progChange = false;                 
}

@Override
public void onPageScrollStateChanged(int arg0) {

}
Pard answered 23/7, 2013 at 20:53 Comment(0)
M
0

Kotlin extension functions:

interface OnPageChangeWithUserInfoListener {
    fun onPageScrolled(
        position: Int,
        positionOffset: Float,
        positionOffsetPixels: Int
    )

    fun onPageSelected(
        position: Int,
        userInitiated: Boolean // New parameter!
    )

    fun onPageScrollStateChanged(state: Int)
}

fun ViewPager.setOnPageChangeListenerWithUserInfo(onPageChangeWithUserInfoListener: OnPageChangeWithUserInfoListener) {
    var previousState: Int = -1
    var userScrollChange = false

    addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
        override fun onPageScrolled(
            position: Int,
            positionOffset: Float,
            positionOffsetPixels: Int
        ) {
            onPageChangeWithUserInfoListener.onPageScrolled(position, positionOffset, positionOffsetPixels)
        }

        override fun onPageSelected(position: Int) {
            onPageChangeWithUserInfoListener.onPageSelected(position, userScrollChange)
        }

        override fun onPageScrollStateChanged(state: Int) {
            if (previousState == ViewPager.SCROLL_STATE_DRAGGING &&
                state == ViewPager.SCROLL_STATE_SETTLING) {
                userScrollChange = true
            } else if (previousState == ViewPager.SCROLL_STATE_SETTLING &&
                state == ViewPager.SCROLL_STATE_IDLE) {
                userScrollChange = false
            }

            previousState = state

            onPageChangeWithUserInfoListener.onPageScrollStateChanged(state)
        }
    })
}
Miscellaneous answered 15/7, 2022 at 0:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.