Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RecyclerView.OnScrollListener: being called multiple times for once instance of scroll

I have a recyclerview with horizontal layout and only one view is visible at a time:

mRecyclerView = findViewById(R.id.rvmain);
mRecyclerView.setOnFlingListener(null);
final SnapHelper snapHelper = new LinearSnapHelper();
snapHelper.attachToRecyclerView(mRecyclerView);
mLayoutManager = new LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false);
mRecyclerView.setLayoutManager(mLayoutManager);
mAdapter = new MainActivityRVAdapter(postsModels,MainActivity.this);
mRecyclerView.setAdapter(mAdapter);

using onScrolllistener, everytime I scroll I want to know the starting position and end position. I am using the below code:

    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);


        if(count == 0) {
            View centerView = snapHelper.findSnapView(mLayoutManager);
            if(centerView != null){
                initial_position = mLayoutManager.getPosition(centerView);
                //initial_position2 = ((LinearLayoutManager)mRecyclerView.getLayoutManager()).findFirstVisibleItemPosition();
                Log.e("Initial Item Position:",""+initial_position);
                //Log.e("Initial Item Position2:",""+initial_position2);
            }
            count ++;
        }



        // get newstate position
        if(newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
            View centerView = snapHelper.findSnapView(mLayoutManager);
            if(centerView != null){
                int pos = mLayoutManager.getPosition(centerView);

                count = 0; // in idle state clear the count again
                Log.e("Snapped Item Position:",""+pos);
            }
        }


    }

The result i get is:

E/Initial Item Position:: 0
E/Snapped Item Position:: 1
E/Initial Item Position:: 1
E/Snapped Item Position:: 1
E/Initial Item Position:: 1
E/Snapped Item Position:: 1
E/Initial Item Position:: 1
E/Snapped Item Position:: 1

And it returns positions multiple times. I wanted to check the difference between final and initial positions. I wanted only the start and end so that i can compare and check i.e:

E/Initial Item Position:: 0 and

E/Snapped Item Position:: 1
like image 228
Santhosh Avatar asked Mar 07 '18 12:03

Santhosh


3 Answers

I've met the same problem. And what i found: RecyclerView.OnScrollListener calls onScrolled(...) multiple times while in SCROLL_STATE_DRAGGING or SCROLL_STATE_SETTLING.

I started listen to a second calback:
onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState)
We are interesting in a final state SCROLL_STATE_IDLE.

So in this case we have to override onScrollStateChanged(...), check and ignore all states except SCROLL_STATE_IDLE and get final position while idle.

But as described in docs, onScrolled(...)

will also be called if visible item range changes after a layout calculation. In that case, dx and dy will be 0.

On practice i found that if we call adapter.notifyDataSetChanged() and visible position is "0" (ie. on first start), onScrollStateChanged(...) will not be called and onScrolled(...) will be called once with dx == dy == 0.

A final variant could be as following:

private int recyclerVisiblePosition;    
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
   super.onScrollStateChanged(recyclerView, newState);

   if (newState != RecyclerView.SCROLL_STATE_IDLE) {
      return;
   }

   getNewPosition(recyclerView);
}

@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
    super.onScrolled(recyclerView, dx, dy);

    if (dx == 0 && dy == 0) {
        getNewPosition(recyclerView);
    } 
}

private void getNewPosition(@NonNull final RecyclerView recyclerView) {
    final LinearLayoutManager layoutManager = ((LinearLayoutManager)recyclerView.getLayoutManager());
    if (layoutManager != null) {
        recyclerVisiblePosition = layoutManager.findLastVisibleItemPosition();
    }
}
like image 54
Sergio Avatar answered Nov 19 '22 08:11

Sergio


Reason:

When you scroll on the RecyclerView, it will trigger a SCROLL_STATE_IDLE event at the end, this is due to your action that you fling, and then you let go the RecyclerView.

The SnapHelper will listen to RecyclerView's first SCROLL_STATE_IDLE event so that it can prepare to calculate the remaining distance that IT needs to scroll to the target View, when it does that, it will trigger a SCROLL_STATE_IDLE event the second time, since SnapHelper is "helping" you to scroll it.

===============================================================

Solution:

You can create a global variable and use it as a "flag", this will avoid it from being called multiple times. The final code will look something like this:

private boolean isFirstTimeCall = true;

recyclerViewExample.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);

            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                if (isFirstTimeCall) {
                    isFirstTimeCall = false;

                    // Do your stuff here...
                }
            }

            if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
                isFirstTimeCall = true;
            }
        }
    });
like image 23
marticztn Avatar answered Nov 19 '22 07:11

marticztn


I had the same issue and I recognized that it was the snap helper which causes the issue. I guess it is because snap helper triggers a small second movement after your scrolling is complete. So onScrollStateChanged is called twice.

Fortunately, now there is ViewPager2. So I didn't have to go nuts trying to listen snapping. As I had also one visible view at a time like you, I replaced Recyclerview with ViewPager2 and instead of Recyclerview.OnScrollListener, I used ViewPager2.OnPageChangeCallback. That solved the issue.

like image 1
Oya Canli Avatar answered Nov 19 '22 09:11

Oya Canli