CommonsWare
CommonsWare

Reputation: 1007265

How Do We Distinguish User Scrolls from scrollToPosition() Scrolls in RecyclerView.OnScrollListener?

In my onScrolled() method of a RecyclerView.OnScrollListener, I need to be able to distinguish between two different sources of scroll events:

There is no parameter to onScrolled() that would appear to cover this. Is there a recipe for making this distinction?

FWIW, my scenario: I have two RecyclerView widgets. One is the master, and shows a traditional vertical list. The other is serving as a ViewPager replacement, to allow for swiping through details of the same items that are in the vertical list. In a large-screen environment, both RecyclerView widgets will be visible at once (master-detail pattern), and I need to keep them synchronized. If the user swipes the pager RecyclerView, I need to change an indicator on the list RecyclerView rows to match. If the user taps on a row in the list RecyclerView, I need to update the current page in the pager RecyclerView. To listen for swipe events on the pager, I am using OnScrollListener, but that also is getting triggered when I scroll the pager in response to list row clicks.

Upvotes: 5

Views: 866

Answers (2)

Wachid Susilo
Wachid Susilo

Reputation: 656

The accepted answer will not work if you are using smoothScrollBy() or smoothScrollToPosition(). The only way to tell if the scroll was from user is to set a flag when the user is dragging, when this happen the RecyclerView state would be changed to RecyclerView.SCROLL_STATE_DRAGGING. Then, when the state changed to RecyclerView.SCROLL_STATE_IDLE, you need to clear the flag.

var scrolledByUser = false

rv.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrollStateChanged(
        recyclerView: RecyclerView,
        newState: Int
    ) {
        when (newState) {
            RecyclerView.SCROLL_STATE_DRAGGING -> {
                scrolledByUser = true
            }
            RecyclerView.SCROLL_STATE_IDLE -> {
                if (scrolledByUser) {
                    // fromUser
                } else {
                    // from smoothScroll
                }
                scrolledByUser = false
            }
        }
    }
})

Upvotes: 1

CommonsWare
CommonsWare

Reputation: 1007265

Gabe Sechan's suggestion of using the scroll-state change is a good one.

Unfortunately, there is no listener for the scroll state — it requires that you subclass RecyclerView. So, I whipped up a RecyclerViewEx to add a listener API for that event:

public class RecyclerViewEx extends RecyclerView {
  interface OnScrollStateChangedListener {
    void onScrollStateChanged(int state);
  }

  final private ArrayList<OnScrollStateChangedListener> listeners=new ArrayList<>();

  public RecyclerViewEx(Context context) {
    super(context);
  }

  public RecyclerViewEx(Context context, @Nullable
    AttributeSet attrs) {
    super(context, attrs);
  }

  public RecyclerViewEx(Context context, @Nullable AttributeSet attrs,
                        int defStyle) {
    super(context, attrs, defStyle);
  }

  public void addOnScrollStateChangedListener(OnScrollStateChangedListener listener) {
    listeners.add(listener);
  }

  public void removeOnScrollStateChangedListener(OnScrollStateChangedListener listener) {
    listeners.remove(listener);
  }

  @Override
  public void onScrollStateChanged(int state) {
    super.onScrollStateChanged(state);

    for (OnScrollStateChangedListener listener : listeners) {
      listener.onScrollStateChanged(state);
    }
  }
}

scrollToPosition() does not trigger a scroll-state change, whereas user swipes do. So, I can distinguish between the two scenarios that way.

rve.addOnScrollStateChangedListener(
  scrollState -> {
    if (scrollState==RecyclerView.SCROLL_STATE_IDLE) {
      // do something cool, now that the user swipe is complete
    }
  });

Given this, I do not need a flag or any other state, which helps keep the logic simpler.

Upvotes: 3

Related Questions