简体   繁体   English

RecyclerView 和 SwipeRefreshLayout

[英]RecyclerView and SwipeRefreshLayout

I'm using the new RecyclerView-Layout in a SwipeRefreshLayout and experienced a strange behaviour.我在SwipeRefreshLayout中使用新的RecyclerView-Layout并遇到了奇怪的行为。 When scrolling the list back to the top sometimes the view on the top gets cut in.当将列表滚动回顶部时,有时顶部的视图会被切入。

列表滚动到顶部

If i try to scroll to the top now - the Pull-To-Refresh triggers.如果我现在尝试滚动到顶部 - Pull-To-Refresh 触发器。

切割行

If i try and remove the Swipe-Refresh-Layout around the Recycler-View the Problem is gone.如果我尝试删除 Recycler-View 周围的 Swipe-Refresh-Layout,问题就消失了。 And its reproducable on any Phone (not only L-Preview devices).并且它可以在任何手机上重现(不仅是 L-Preview 设备)。

 <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).这是我的布局 - 行是由 RecyclerViewAdapter 动态构建的(此列表中有 2 个视图类型)。

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 .以及包含RecyclerSwipeRefreshLayoutFragment的代码。

If anyone else has experienced this behaviour and solved it or at least found the reason for it?如果其他人经历过这种行为并解决了它,或者至少找到了原因?

write the following code in addOnScrollListener of the RecyclerViewRecyclerView addOnScrollListener中编写如下代码

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

Before you use this solution: RecyclerView is not complete yet, TRY NOT TO USE IT IN PRODUCTION UNLESS YOU'RE LIKE ME! 在您使用此解决方案之前:RecyclerView 尚未完成,除非您像我一样,否则尽量不要在生产中使用它!

As for November 2014, there are still bugs in RecyclerView that would cause canScrollVertically to return false prematurely.至于 2014 年 11 月,RecyclerView 中仍然存在会导致canScrollVertically过早返回 false 的错误。 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!你甚至都不需要更换RecyclerView在你的代码FixedRecyclerView ,取代了XML标签就足够了! (The ensures that when RecyclerView is complete, the transition would be quick and simple) (确保当 RecyclerView 完成时,转换会快速而简单)

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.基本上, canScrollVertically(boolean)过早返回 false,因此我们检查RecyclerView是否一直滚动到第一个视图的顶部(第一个子视图的顶部将为 0)然后返回。

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.编辑:如果您出于某种原因不想扩展RecyclerView ,您可以扩展SwipeRefreshLayout并覆盖canChildScrollUp()方法并将检查逻辑放在那里。

EDIT2: RecyclerView has been released and so far there's no need to use this fix. EDIT2: RecyclerView 已经发布,到目前为止还没有必要使用这个修复程序。

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.我尝试了@Krunal_Patel 建议的方法,但它在我的 Nexus 4 中大部分时间都有效,而在三星 Galaxy s2 中根本无效。 While debugging, recyclerView.getChildAt(0).getTop() is always not correct for RecyclerView.在调试时,recyclerView.getChildAt(0).getTop() 对于 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.所以,经过各种方法,我想我们可以利用LayoutManager的findFirstCompletelyVisibleItemPosition()方法来预测RecyclerView的第一项是否可见,启用SwipeRefreshLayout。找到下面的代码。 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);
        }
    });

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.我希望这绝对可以帮助正在寻找类似解决方案的人。

Source Code https://drive.google.com/open?id=0BzBKpZ4nzNzURkRGNVFtZXV1RWM源代码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);
    }
});

unfortunately, this is a known bug in LinearLayoutManager.不幸的是,这是 LinearLayoutManager 中的一个已知错误。 It does not computeScrollOffset properly when the first item is visible.当第一项可见时,它不会正确计算ScrollOffset。 will be fixed when it is released.发布时会修复。

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 .我通过添加滚动侦听器来解决它,该侦听器将等到在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.在您使用标题视图持有者的情况下应启用SwipeRefreshLayout时,将添加预期的第一个可见值以将其用作阈值位置。

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

I run into the same problem.我遇到了同样的问题。 My solution is overriding onScrolled method of OnScrollListener .我的解决方案是覆盖OnScrollListener onScrolled方法。

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

Here's one way to handle this, which also handles ListView/GridView.这是处理此问题的一种方法,它也处理 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);
    }
  }

The krunal's solution is good, but it works like hotfix and does not cover some specific cases, for example this one: krunal 的解决方案很好,但它像修补程序一样工作,不包括某些特定情况,例如这个:

Let's say that the RecyclerView contains an EditText at the middle of screen.假设 RecyclerView 在屏幕中间包含一个 EditText。 We start application (topRowVerticalPosition = 0), taps on the EditText.我们启动应用程序 (topRowVerticalPosition = 0),点击 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.结果,软键盘出现,RecyclerView的大小减小,系统自动滚动以保持EditText可见并且topRowVerticalPosition不应为0,但不调用onScrolled并且不重新计算topRowVerticalPosition。

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.在您指定内部 RecyclerView 到 SupportSwipeRefreshLayout 后,如果 RecyclerView 无法向上滚动,它会自动将触摸事件发送到 SupportSwipeRefreshLayout,否则发送到 RecyclerView。

Single line solution.单线解决方案。

setOnScrollListener is deprecated. setOnScrollListener已弃用。

You can use setOnScrollChangeListener for same purspose like this :您可以将setOnScrollChangeListener用于相同的目的,如下所示:

recylerView.setOnScrollChangeListener((view, i, i1, i2, i3) -> swipeToRefreshLayout.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 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.似乎 SwipeRefreshLayout 与具有 1 个以上项目类型的适配器不兼容。

None of the answers worked for me, but I managed to implement my own solution by making a custom implementation of LinearLayoutManager.没有一个答案对我有用,但是我通过自定义 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!它正在工作!

If you are using recyclerview without scrollview you can do this and it will work如果您使用没有滚动视图的 recyclerview,您可以这样做,它会起作用

        recyclerview.isNestedScrollingEnabled = true

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM