Lukas Olsen
Lukas Olsen

Reputation: 5464

RecyclerView and SwipeRefreshLayout

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?

Upvotes: 65

Views: 91111

Answers (14)

Rohan majhi
Rohan majhi

Reputation: 131

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

        recyclerview.isNestedScrollingEnabled = true

Upvotes: 1

Maria Rodionova
Maria Rodionova

Reputation: 56

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!

Upvotes: 1

Keshav Gera
Keshav Gera

Reputation: 11244

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);
    }
});

Upvotes: 2

Mathieu
Mathieu

Reputation: 1703

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.

Upvotes: 0

SANAT
SANAT

Reputation: 9257

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));

Upvotes: 0

Amrut Bidri
Amrut Bidri

Reputation: 6360

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.

Upvotes: 11

alcsan
alcsan

Reputation: 6242

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.

Upvotes: 0

krunal patel
krunal patel

Reputation: 2499

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);
        }
    });

Upvotes: 75

android developer
android developer

Reputation: 116322

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);
    }
  }

Upvotes: 0

SilentKnight
SilentKnight

Reputation: 14011

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);
        }
    }
});

Upvotes: 0

balachandarkm
balachandarkm

Reputation: 855

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);
        }
    });

Upvotes: 13

Nikola Despotoski
Nikola Despotoski

Reputation: 50538

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);

Upvotes: 0

tom91136
tom91136

Reputation: 8962

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.

Upvotes: 53

yigit
yigit

Reputation: 38243

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.

Upvotes: 0

Related Questions