65

I'm using the new RecyclerView-Layout in a SwipeRefreshLayout and experienced a strange behaviour. When scrolling the list back to the top sometimes the view on the top gets cut in.

List scrolled to the top

If i try to scroll to the top now - the Pull-To-Refresh triggers.

cutted row

If i try and remove the Swipe-Refresh-Layout around the Recycler-View the Problem is gone. And its reproducable on any Phone (not only 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>

That's my layout - the rows are built dynamically by the RecyclerViewAdapter (2 Viewtypes 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's 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 code of the Fragment containing the Recycler and the SwipeRefreshLayout.

If anyone else has experienced this behaviour and solved it or at least found the reason for it?

Tom11
  • 2,419
  • 8
  • 30
  • 56
Lukas Olsen
  • 5,294
  • 7
  • 22
  • 28
  • I had the exactly same problem. Further I noticed that RecyclerView always returns 0 from getScrollY(). Maybe this is logical as various LayoutManagers can be swapped in, not necesserily scrolling ones. I'm not shure because I didn't research further. I'm happy to find a solution, but will upvote the answer when I can test it after my vacation in two weeks. – cybergen Aug 14 '14 at 18:10
  • 2
    Lukas Olsen that "textured" looks incredible. – Jared Burrows Oct 29 '14 at 03:06

14 Answers14

75

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);
        }
    });
esilver
  • 27,713
  • 23
  • 122
  • 168
krunal patel
  • 2,369
  • 1
  • 11
  • 11
  • 2
    i'm using recyclerview - not listview – Lukas Olsen Aug 08 '14 at 10:58
  • 2
    just to extend the answer a bit: in case you set padding for your items by using `addItemDecoration` and also have a top padding on the `ReyclerView` itself you would toggle your `SwipeRefreshLayout` as following: `boolean canEnableSwipeRefresh = mLayoutManager.findFirstVisibleItemPosition() == 0 && recyclerView.getChildAt(0).getTop() - (offset + mRecView.getPaddingTop()) == 0;` where `offset` if the top padding of your items – Droidman Dec 10 '14 at 11:53
  • @krunalpatel Your parameters to onScrollStateChanged are wrong. They should be: onScrollStateChanged(RecyclerView recyclerView, int scrollState) – IgorGanapolsky Jan 07 '15 at 20:43
  • recyclerView.setOnScrollListener is depreciated, use addOnScrollListener instead – jiawen Nov 11 '15 at 05:31
53

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.

tom91136
  • 8,662
  • 12
  • 58
  • 74
  • I've updated the code to fix a case where the the adapter returns 0 for getCount() – tom91136 Aug 11 '14 at 14:56
  • 1
    I think it should be: `if (direction < 0)` – cybergen Sep 03 '14 at 08:22
  • This does not work for me. That method does not even get called if the list is already displaying the center of data in the list. When I pull down it just refreshes. – Markymark Dec 28 '14 at 00:04
  • 1
    So you're saying that `canScrollVertically` is not called anywhere at all? Then you might still be using the library version of RecyclerView. Add some `Log` to the constructor and see if that's called at all. – tom91136 Dec 28 '14 at 00:32
  • 2
    I was seeing the same thing as @Marky17, the method was not being called. After digging, the reason was my FixedRecyclerView (FRV) was not a DIRECT descendant of the SwipeRefreshLayout (SRL). My FRV was inside of a FrameLayout (so I could add a QuickReturn bar). So, when SRL tried to ask its child "CanScrollVertically?" it was asking the enclosing FrameLayout, not the FRV. Your approach works perfectly if the FRV is the (only?) direct child of the SRL. For my app, I've tweaked SRL to check for an FRV child if it encounters a FrameLayout. Thanks! – MojoTosh Jan 02 '15 at 17:39
  • This worked for me: return getChildAt(0).getTop() != 0 – user1354603 Jan 23 '15 at 10:01
  • 1
    Just to add to this, if your recyclerview has padding added to the top for quick return toolbar pattern then we need to account for this by changing getChildAt(0).getTop() < 0 to getChildAt(0).getTop() < getPaddingTop() – Steve G. Feb 03 '15 at 20:13
  • canScrollVertically(direction) was exactly what i was looking for – Penzzz Jun 11 '15 at 09:51
13

I came across the same problem recently. I tried the approach suggested by @Krunal_Patel, But It worked most of the times in my Nexus 4 and didn't work at all in samsung galaxy s2. While debugging, recyclerView.getChildAt(0).getTop() is always not correct for RecyclerView. So, After going through various methods, I figured that we can make use of the method findFirstCompletelyVisibleItemPosition() of the LayoutManager to predict whether the first item of the RecyclerView is visible or not, to enable SwipeRefreshLayout.Find the code below. Hope it helps someone trying to fix the same issue. Cheers.

    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);
        }
    });
Jared Burrows
  • 54,294
  • 25
  • 151
  • 185
balachandarkm
  • 855
  • 2
  • 9
  • 11
  • While I don't agree with using a scroll listener I used the same condition overriding the `SwipeRefreshLayout`'s `canChildScrollUp` method and it seems to work well. – darnmason Nov 26 '14 at 11:55
  • Version 21.0.2 of AppCompat removed the need for any workaround in my case – darnmason Feb 08 '15 at 12:44
  • 1
    @darnmason I tried with 21.0.2. But it is still the same for me. It didn't work. – balachandarkm Feb 25 '15 at 11:37
  • `linearLayoutManager.findFirstCompletelyVisibleItemPosition()` always returns zero for me! – Saravanabalagi Ramachandran Apr 09 '16 at 16:16
  • This will fail if no item is completely visible. Use this `int firstPos=linearLayoutManager.findFirstVisibleItemPosition(); if (firstPos>0) { refreshLayout.setEnabled(false); } else { if(linearLayoutManager.findFirstCompletelyVisibleItemPosition()>0) refreshLayout.setEnabled(true); }` – jatin rana Dec 02 '18 at 14:25
11

This is how I have resolved this issue in my case. It might be useful for someone else who end up here for searching solutions similar to 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 might definitely help someone who are looking for similar solution.

Srikanth R
  • 410
  • 5
  • 17
Amrut Bidri
  • 6,276
  • 6
  • 38
  • 80
  • This will fail if no item is completely visible. Use this `int firstPos=linearLayoutManager.findFirstVisibleItemPosition(); if (firstPos>0) { refreshLayout.setEnabled(false); } else { if(linearLayoutManager.findFirstCompletelyVisibleItemPosition()>0) refreshLayout.setEnabled(true); }` – jatin rana Dec 02 '18 at 14:27
2

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);
    }
});
Boken
  • 4,825
  • 10
  • 32
  • 42
Keshav Gera
  • 10,807
  • 1
  • 75
  • 53
  • 2
    Please consider using some sort of open-source file hosting service (such as GitHub or BitBucket) instead of zipping your whole project and uploading it to Google Drive. – Edric Dec 10 '18 at 06:59
1

None of the answers worked for me, but I managed to implement my own solution by making a custom implementation of LinearLayoutManager. Posting it here in case someone else needs it.

class LayoutManagerScrollFixed(context: Context) : LinearLayoutManager(context) {

override fun smoothScrollToPosition(
    recyclerView: RecyclerView?,
    state: RecyclerView.State?,
    position: Int
) {
    super.smoothScrollToPosition(recyclerView, state, position)
    val child = getChildAt(0)
    if (position == 0 && recyclerView != null && child != null) {
        scrollVerticallyBy(child.top - recyclerView.paddingTop, recyclerView.Recycler(), state)
    }
}

Then, you just call

recyclerView?.layoutManager = LayoutManagerScrollFixed(requireContext())

And it's working!

1

If you are using recyclerview without scrollview you can do this and it will work

        recyclerview.isNestedScrollingEnabled = true
Rohan majhi
  • 121
  • 1
  • 4
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.

yigit
  • 37,683
  • 13
  • 72
  • 58
  • This doesn't seem to have been fixed – darnmason Nov 26 '14 at 11:06
  • I created a sample app and works fine. Can you create a sample app and a bug report on b.android.com ? Thanks. – yigit Dec 01 '14 at 19:03
  • My layout is quite complex and while trying to reproduce the issue in a sample app I realised there was a minor release 21.0.2 of AppCompat which has fixed it. :D – darnmason Dec 02 '14 at 12:22
  • That's great news, thanks for checking. İ strongly advice updating your main project as well because there are other bug fixes too. – yigit Dec 02 '14 at 16:51
0

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);
Nikola Despotoski
  • 49,966
  • 15
  • 119
  • 148
0

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);
        }
    }
});
SilentKnight
  • 13,761
  • 19
  • 49
  • 78
0

Here's one way to handle this, which also handles 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);
    }
  }
android developer
  • 114,585
  • 152
  • 739
  • 1,270
0

The krunal's solution is good, but it works like hotfix and does not cover some specific cases, for example this one:

Let's say that the RecyclerView contains an EditText at the middle of screen. We start application (topRowVerticalPosition = 0), taps on the EditText. As result, software keyboard shows up, size of the RecyclerView is decreased, it is automatically scrolled by system to keep the EditText visible and topRowVerticalPosition should not be 0, but onScrolled is not called and topRowVerticalPosition is not recalculated.

Therefore, I suggest 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 specify internal RecyclerView to SupportSwipeRefreshLayout, it will automatically send touch event to SupportSwipeRefreshLayout if RecyclerView cannot be scrolled up and to RecyclerView otherwise.

alcsan
  • 6,172
  • 1
  • 23
  • 19
0

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));
SANAT
  • 8,489
  • 55
  • 66
0

In case of someone find this question and is not satisfied by the answer :

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

Mathieu
  • 1,435
  • 3
  • 16
  • 35