I\'m using the new RecyclerView-Layout
in a SwipeRefreshLayout
and experienced a strange behaviour. When scrolling the list back to the top sometim
I run into the same problem. My solution is overriding onScrolled
method of OnScrollListener
.
Workaround is here:
recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int offset = dy - ydy;//to adjust scrolling sensitivity of calling OnRefreshListener
ydy = dy;//updated old value
boolean shouldRefresh = (linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0)
&& (recyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) && offset > 30;
if (shouldRefresh) {
swipeRefreshLayout.setRefreshing(true);
} else {
swipeRefreshLayout.setRefreshing(false);
}
}
});
Single line solution.
setOnScrollListener is deprecated.
You can use setOnScrollChangeListener for same purspose like this :
recylerView.setOnScrollChangeListener((view, i, i1, i2, i3) -> swipeToRefreshLayout.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0));
unfortunately, this is a known bug in LinearLayoutManager. It does not computeScrollOffset properly when the first item is visible. will be fixed when it is released.
Before you use this solution:
RecyclerView is not complete yet, TRY NOT TO USE IT IN PRODUCTION UNLESS YOU'RE LIKE ME!
As for November 2014, there are still bugs in RecyclerView that would cause canScrollVertically
to return false prematurely. This solution will resolve all scrolling problems.
The drop in solution:
public class FixedRecyclerView extends RecyclerView {
public FixedRecyclerView(Context context) {
super(context);
}
public FixedRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FixedRecyclerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean canScrollVertically(int direction) {
// check if scrolling up
if (direction < 1) {
boolean original = super.canScrollVertically(direction);
return !original && getChildAt(0) != null && getChildAt(0).getTop() < 0 || original;
}
return super.canScrollVertically(direction);
}
}
You don't even need to replace RecyclerView
in your code with FixedRecyclerView
, replacing the XML tag would be sufficient! (The ensures that when RecyclerView is complete, the transition would be quick and simple)
Explanation:
Basically, canScrollVertically(boolean)
returns false too early,so we check if the RecyclerView
is scrolled all the way to the top of the first view (where the first child's top would be 0) and then return.
EDIT:
And if you don't want to extend RecyclerView
for some reason, you can extend SwipeRefreshLayout
and override the canChildScrollUp()
method and put the checking logic in there.
EDIT2:
RecyclerView has been released and so far there's no need to use this fix.
write the following code in addOnScrollListener
of the RecyclerView
Like this:
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
int topRowVerticalPosition =
(recyclerView == null || recyclerView.getChildCount() == 0) ? 0 : recyclerView.getChildAt(0).getTop();
swipeRefreshLayout.setEnabled(topRowVerticalPosition >= 0);
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
});
I have experienced same issue. I solved it by adding scroll listener that will wait until expected first visible item is drawn on the RecyclerView
. You can bind other scroll listeners too, along this one. Expected first visible value is added to use it as threshold position when the SwipeRefreshLayout
should be enabled in cases where you use header view holders.
public class SwipeRefreshLayoutToggleScrollListener extends RecyclerView.OnScrollListener {
private List<RecyclerView.OnScrollListener> mScrollListeners = new ArrayList<RecyclerView.OnScrollListener>();
private int mExpectedVisiblePosition = 0;
public SwipeRefreshLayoutToggleScrollListener(SwipeRefreshLayout mSwipeLayout) {
this.mSwipeLayout = mSwipeLayout;
}
private SwipeRefreshLayout mSwipeLayout;
public void addScrollListener(RecyclerView.OnScrollListener listener){
mScrollListeners.add(listener);
}
public boolean removeScrollListener(RecyclerView.OnScrollListener listener){
return mScrollListeners.remove(listener);
}
public void setExpectedFirstVisiblePosition(int position){
mExpectedVisiblePosition = position;
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
notifyScrollStateChanged(recyclerView,newState);
LinearLayoutManager llm = (LinearLayoutManager) recyclerView.getLayoutManager();
int firstVisible = llm.findFirstCompletelyVisibleItemPosition();
if(firstVisible != RecyclerView.NO_POSITION)
mSwipeLayout.setEnabled(firstVisible == mExpectedVisiblePosition);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
notifyOnScrolled(recyclerView, dx, dy);
}
private void notifyOnScrolled(RecyclerView recyclerView, int dx, int dy){
for(RecyclerView.OnScrollListener listener : mScrollListeners){
listener.onScrolled(recyclerView, dx, dy);
}
}
private void notifyScrollStateChanged(RecyclerView recyclerView, int newState){
for(RecyclerView.OnScrollListener listener : mScrollListeners){
listener.onScrollStateChanged(recyclerView, newState);
}
}
}
Usage:
SwipeRefreshLayoutToggleScrollListener listener = new SwipeRefreshLayoutToggleScrollListener(mSwiperRefreshLayout);
listener.addScrollListener(this); //optional
listener.addScrollListener(mScrollListener1); //optional
mRecyclerView.setOnScrollLIstener(listener);