RecyclerView and SwipeRefreshLayout

I use the new RecyclerView-Layout in SwipeRefreshLayout and experience strange behavior. When scrolling up the list, sometimes top view.

List scrolled to the top

If I try to scroll up to the end - Pull-To-Refresh triggers.

cutted row

If I try to remove the Shipe-Refresh-Layout around the Recycler-View, the problem will disappear. And its playable on any phone (not just L-Preview devices).

  <android.support.v4.widget.SwipeRefreshLayout android:id="@+id/contentView" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone"> <android.support.v7.widget.RecyclerView android:id="@+id/hot_fragment_recycler" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.v4.widget.SwipeRefreshLayout> 

What is my layout - rows are built dynamically using the RecyclerViewAdapter (2 views in this list).

 public class HotRecyclerAdapter extends TikDaggerRecyclerAdapter<GameRow> { private static final int VIEWTYPE_GAME_TITLE = 0; private static final int VIEWTYPE_GAME_TEAM = 1; @Inject Picasso picasso; public HotRecyclerAdapter(Injector injector) { super(injector); } @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position, int viewType) { switch (viewType) { case VIEWTYPE_GAME_TITLE: { TitleGameRowViewHolder holder = (TitleGameRowViewHolder) viewHolder; holder.bindGameRow(picasso, getItem(position)); break; } case VIEWTYPE_GAME_TEAM: { TeamGameRowViewHolder holder = (TeamGameRowViewHolder) viewHolder; holder.bindGameRow(picasso, getItem(position)); break; } } } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { switch (viewType) { case VIEWTYPE_GAME_TITLE: { View view = inflater.inflate(R.layout.game_row_title, viewGroup, false); return new TitleGameRowViewHolder(view); } case VIEWTYPE_GAME_TEAM: { View view = inflater.inflate(R.layout.game_row_team, viewGroup, false); return new TeamGameRowViewHolder(view); } } return null; } @Override public int getItemViewType(int position) { GameRow row = getItem(position); if (row.isTeamGameRow()) { return VIEWTYPE_GAME_TEAM; } return VIEWTYPE_GAME_TITLE; } 

Here is the adapter.

  hotAdapter = new HotRecyclerAdapter(this); recyclerView.setHasFixedSize(false); recyclerView.setAdapter(hotAdapter); recyclerView.setItemAnimator(new DefaultItemAnimator()); recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); contentView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { loadData(); } }); TypedArray colorSheme = getResources().obtainTypedArray(R.array.main_refresh_sheme); contentView.setColorSchemeResources(colorSheme.getResourceId(0, -1), colorSheme.getResourceId(1, -1), colorSheme.getResourceId(2, -1), colorSheme.getResourceId(3, -1)); 

And the Fragment code containing Recycler and SwipeRefreshLayout .

Should someone else experience this behavior and solve it, or at least find a reason for it?

+57
android android-recyclerview swiperefreshlayout
Aug 07 '14 at 9:01
source share
12 answers

Before using this solution: RecyclerView is not finished yet, DO NOT USE IT IN PRODUCTION IF YOU ARE NOT ME!

As in November 2014, there are still errors in RecyclerView that canScrollVertically returns false prematurely. This solution will solve all scrolling problems.

Solution drop:

 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 will be enough! (This ensures that when the RecyclerView is completed, the transition will be quick and easy)

Explanation:

Basically, canScrollVertically(boolean) returns false too early, so we check to see if the RecyclerView scrolls to the very first view (where the first top of the top level is 0), and then returns.

EDIT: And if for some reason you do not want to extend the RecyclerView , you can extend the SwipeRefreshLayout and override the canChildScrollUp() method and put the test logic there.

EDIT2: RecyclerView has been released and there is still no need to use this hotfix.

+52
Aug 10 '14 at 10:44
source share

write the following code in addOnScrollListener 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); } }); 
+65
Aug 07 '14 at 13:21
source share

I recently ran into the same problem. I tried the approach suggested by @Krunal_Patel, but it worked most of the time in my Nexus 4 and didn't work at all in the s2 ssung galaxy. When debugging, recyclerView.getChildAt (0) .getTop () is always not suitable for RecyclerView. So, after going through various methods, I decided that we can use the findFirstCompletelyVisibleItemPosition () LayoutManager method to predict whether the first RecyclerView element is visible or not to enable SwipeRefreshLayout. Find the code below. Hope this helps someone who is trying to solve the same problem. Greetings.

  recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { public void onScrollStateChanged(RecyclerView recyclerView, int newState) { } public void onScrolled(RecyclerView recyclerView, int dx, int dy) { swipeRefresh.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0); } }); 
+12
02 Oct '14 at 11:53 on
source share

This is how I solved this problem in my case. It might be useful for someone else who is here to find solutions like this.

 recyclerView.addOnScrollListener(new OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { // TODO Auto-generated method stub super.onScrolled(recyclerView, dx, dy); } @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { // TODO Auto-generated method stub //super.onScrollStateChanged(recyclerView, newState); int firstPos=linearLayoutManager.findFirstCompletelyVisibleItemPosition(); if (firstPos>0) { swipeLayout.setEnabled(false); } else { swipeLayout.setEnabled(true); } } }); 

I hope this can definitely help someone who is looking for a similar solution.

+10
Apr 7 '15 at 6:46
source share

Source code https://drive.google.com/open?id=0BzBKpZ4nzNzURkRGNVFtZXV1RWM

recyclerView.setOnScrollListener (new RecyclerView.OnScrollListener () {

  public void onScrollStateChanged(RecyclerView recyclerView, int newState) { } public void onScrolled(RecyclerView recyclerView, int dx, int dy) { swipeRefresh.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0); } }); 
+1
02 Sep '17 at 10:19 on
source share

Unfortunately, this is a known bug in the LinearLayoutManager. It does not calculate ScrollOffset correctly when the first element is visible. will be fixed when it is released.

0
Aug 08 '14 at 21:23
source share

I had the same problem. I solved this by adding a scroll listener that would wait until the expected first visible element was drawn on the RecyclerView . You can associate other scroll listeners along this. The expected first visible value is added to use it as a threshold position when SwipeRefreshLayout should be enabled in cases where you are using header 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); } } } 

Using:

 SwipeRefreshLayoutToggleScrollListener listener = new SwipeRefreshLayoutToggleScrollListener(mSwiperRefreshLayout); listener.addScrollListener(this); //optional listener.addScrollListener(mScrollListener1); //optional mRecyclerView.setOnScrollLIstener(listener); 
0
Nov 27 '14 at 15:14
source share

I am facing the same problem. My solution overrides the onScrolled OnScrollListener method.

Workaround 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); } } }); 
0
Jun 10 '15 at 7:26
source share

Here's one way to handle this, which also handles a ListView / GridView.

 public class SwipeRefreshLayout extends android.support.v4.widget.SwipeRefreshLayout { public SwipeRefreshLayout(Context context) { super(context); } public SwipeRefreshLayout(Context context,AttributeSet attrs) { super(context,attrs); } @Override public boolean canChildScrollUp() { View target=getChildAt(0); if(target instanceof AbsListView) { final AbsListView absListView=(AbsListView)target; return absListView.getChildCount()>0 &&(absListView.getFirstVisiblePosition()>0||absListView.getChildAt(0) .getTop()<absListView.getPaddingTop()); } else return ViewCompat.canScrollVertically(target,-1); } } 
0
Nov 28 '15 at 23:24
source share

The krunal solution is good, but it works as a fix and does not cover some specific cases, for example:

Say that RecyclerView contains an EditText in the middle of the screen. We launch the application (topRowVerticalPosition = 0), click on EditText. As a result, a soft keyboard appears, the size of the RecyclerView decreases, it is automatically scrolled by the system to keep the EditText visible, and topRowVerticalPosition should not be 0, but onScrolled is not called, and topRowVerticalPosition is not recalculated.

Therefore, I propose this solution:

 public class SupportSwipeRefreshLayout extends SwipeRefreshLayout { private RecyclerView mInternalRecyclerView = null; public SupportSwipeRefreshLayout(Context context) { super(context); } public SupportSwipeRefreshLayout(Context context, AttributeSet attrs) { super(context, attrs); } public void setInternalRecyclerView(RecyclerView internalRecyclerView) { mInternalRecyclerView = internalRecyclerView; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (mInternalRecyclerView.canScrollVertically(-1)) { return false; } return super.onInterceptTouchEvent(ev); } } 

After you point the internal RecyclerView to SupportSwipeRefreshLayout, it will automatically send a touch event to SupportSwipeRefreshLayout if the RecyclerView cannot scroll in the RecyclerView otherwise.

0
Mar 07 '16 at 15:11
source share

Single line solution.

setOnScrollListener is deprecated.

You can use setOnScrollChangeListener for the same purpose as this:

 recylerView.setOnScrollChangeListener((view, i, i1, i2, i3) -> swipeToRefreshLayout.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0)); 
0
Apr 2 '18 at 13:39 on
source share

In case someone finds this question and is not satisfied with the answer:

It seems that SwipeRefreshLayout is not compatible with adapters that have more than 1 item type.

0
Jun 11 '19 at 14:17
source share



All Articles