Abbas Abbas - 4 months ago 119
Android Question

RecyclerView - Scroll To Position Not Working Every Time

I have implemented a horizontal scrollable

RecyclerView
. My
RecyclerView
uses a
LinearLayoutManager
, and the problem I am facing is that when I try to use
scrollToPosition(position)
or
smoothScrollToPosition(position)
or from
LinearLayoutManager
's
scrollToPositionWithOffset(position)
. Neither works for me. Either a scroll call doesn't scroll to the desired location or it doesn't invoke the
OnScrollListener
.

So far I have tried so many different combinations of code that I cannot post them all here. Following is the one that works for me (But only partially):

public void smoothUserScrollTo(final int position) {

if (position < 0 || position > getAdapter().getItemCount()) {
Log.e(TAG, "An attempt to scroll out of adapter size has been stopped.");
return;
}

if (getLayoutManager() == null) {
Log.e(TAG, "Cannot scroll to position a LayoutManager is not set. " +
"Call setLayoutManager with a non-null layout.");
return;
}

if (getChildAdapterPosition(getCenterView()) == position) {
return;
}

stopScroll();

scrollToPosition(position);

if (lastScrollPosition == position) {

addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {

if (left == oldLeft && right == oldRight && top == oldTop && bottom == oldBottom) {
removeOnLayoutChangeListener(this);

updateViews();

// removing the following line causes a position - 3 effect.
scrollToView(getChildAt(0));
}
}
});
}

lastScrollPosition = position;
}

@Override
public void scrollToPosition(int position) {
if (position < 0 || position > getAdapter().getItemCount()) {
Log.e(TAG, "An attempt to scroll out of adapter size has been stopped.");
return;
}

if (getLayoutManager() == null) {
Log.e(TAG, "Cannot scroll to position a LayoutManager is not set. " +
"Call setLayoutManager with a non-null layout.");
return;
}

// stopScroll();

((LinearLayoutManager) getLayoutManager()).scrollToPositionWithOffset(position, 0);
// getLayoutManager().scrollToPosition(position);
}


I opted for
scrollToPositionWithOffset()
because of this but the case perhaps is different as I use a LinearLayoutManager instead of GridLayoutManager. But the solution does work for me too, but as I said earlier only partially.


  • When the call to scroll is from 0th position to totalSize - 7 scroll works like a charm.

  • When scroll is from totalSize - 7 to totalSize - 3, First time I only scroll to 7th last item in the list. The second time however I can scroll fine

  • When scrolling from totalSize - 3 to totalSize, I start getting unexpected behavior.



If anyone has found a work around I'd Appreciate it. Here's the gist to my code of custom
ReyclerView
.

Answer

Well, I have found a work around.

Since scrollToPositionWithOffset(position, offset) was the best call I had, I am using it. I have tested output on various data sets and so far has not found any inconsistencies. Hopefully there isn't (But if anyone finds any please comment below).

But then there is the question of last 7 Items that the scroller is somehow unable to scroll to. For those Unfortunate items I am now using smoothScrollBy(dx, dy). But since it can only be used if you know the position of the scrollto-item it becomes a little tricky. But here's the solution (In the above mentioned gist I have changed only the following function definition).

public void smoothUserScrollTo(final int position) {

    if (position < 0 || position > getAdapter().getItemCount()) {
        Log.e(TAG, "An attempt to scroll out of adapter size has been stopped.");
        return;
    }

    if (getLayoutManager() == null) {
        Log.e(TAG, "Cannot scroll to position a LayoutManager is not set. " +
                "Call setLayoutManager with a non-null layout.");
        return;
    }

    if (getChildAdapterPosition(getCenterView()) == position) {
        scrollToView(getCenterView());
        return;
    }

    stopScroll();

    scrollToPosition(position);

    if (lastScrollPosition == position || position >= getAdapter().getItemCount() - 7) {

        addOnLayoutChangeListener(new OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {

                if (left == oldLeft && right == oldRight && top == oldTop && bottom == oldBottom) {

                    if (getChildAdapterPosition(getCenterView()) == position) {
                        //  Only remove the listener if/when the centered items is the item we want to scroll to.
                        removeOnLayoutChangeListener(this);
                        scrollToView(getCenterView());
                        return;
                    }

                    if (position >= 0 && position < getAdapter().getItemCount() - 7) {

                        removeOnLayoutChangeListener(this);
                        updateViews();
                        scrollToView(getChildAt(0));
                    }
                    else if (position >= getAdapter().getItemCount() - 7 && position <= getAdapter().getItemCount()){

                        //  Search in the attached items of the RecyclerView for our required item.
                        //  You can remove the loop and optimize your code further if you want to,
                        //  but I am gonna leave it here as is. It is simple enough :-).
                        int childPosition = 0;
                        for (int i = 0; i < getChildCount(); i++) {
                            childPosition = getChildAdapterPosition(getChildAt(i));
                            if (childPosition == position) {
                                updateViews();
                                scrollToView(getChildAt(i));
                                break;
                            }

                            //  Since we couldn't find the item in attached items.
                            //  Scroll to the last attached item (It'll dettach and attach
                            //  necessary items). So we can scroll once the view is stable again.
                            if (i == getChildCount() - 2) {
                                scrollToView(getChildAt(i));
                            }
                        }
                    }
                }
            }
        });
    }

    lastScrollPosition = position;
}

Note: I am only using smoothScrollBy(dx, dy) for last 7 items. Also any changes are welcome.