【问题标题】:RecyclerView and SwipeRefreshLayoutRecyclerView 和 SwipeRefreshLayout
【发布时间】:2014-09-30 10:44:46
【问题描述】:

我在SwipeRefreshLayout 中使用新的RecyclerView-Layout 并遇到了奇怪的行为。当将列表滚动回顶部时,有时顶部的视图会被切入。

如果我现在尝试滚动到顶部 - 下拉刷新触发器。

如果我尝试删除 Recycler-View 周围的 Swipe-Refresh-Layout,问题就消失了。并且它可以在任何手机上重现(不仅是 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>

这是我的布局 - 行由 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;
}

这是适配器。

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

Fragment 的代码包含RecyclerSwipeRefreshLayout

如果其他人经历过这种行为并解决了它,或者至少找到了原因?

【问题讨论】:

  • 我遇到了完全相同的问题。此外,我注意到 RecyclerView 总是从 getScrollY() 返回 0。也许这是合乎逻辑的,因为可以交换各种 LayoutManager,而不是滚动的。我不舒尔,因为我没有进一步研究。我很高兴找到一个解决方案,但如果我能在两周后的假期后对其进行测试,我会支持这个答案。
  • 卢卡斯·奥尔森的“质感”看起来令人难以置信。

标签: android swiperefreshlayout android-recyclerview


【解决方案1】:

RecyclerViewaddOnScrollListener中写入如下代码

像这样:

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

【讨论】:

  • 我正在使用 recyclerview - 不是 listview
  • 只是为了扩展答案:如果您使用addItemDecoration 为您的项目设置填充并且在ReyclerView 本身也有一个顶部填充,您将切换您的SwipeRefreshLayout 如下: boolean canEnableSwipeRefresh = mLayoutManager.findFirstVisibleItemPosition() == 0 &amp;&amp; recyclerView.getChildAt(0).getTop() - (offset + mRecView.getPaddingTop()) == 0; where offset 如果您的项目的顶部填充
  • @krunalpatel 您对 onScrollStateChanged 的​​参数是错误的。它们应该是:onScrollStateChanged(RecyclerView recyclerView, int scrollState)
  • recyclerView.setOnScrollListener 已弃用,请改用 addOnScrollListener
【解决方案2】:

在使用此解决方案之前: RecyclerView 尚未完成,除非您像我一样,否则尽量不要在生产中使用它!

至于 2014 年 11 月,RecyclerView 中仍然存在会导致 canScrollVertically 过早返回 false 的错误。此解决方案将解决所有滚动问题。

解决方案:

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

    }
}

您甚至不需要将代码中的RecyclerView 替换为FixedRecyclerView,替换XML 标记就足够了! (确保当 RecyclerView 完成时,转换会快速而简单)

解释:

基本上,canScrollVertically(boolean) 返回 false 太早,所以我们检查 RecyclerView 是否一直滚动到第一个视图的顶部(第一个子视图的顶部为 0)然后返回。

编辑: 如果你因为某种原因不想扩展RecyclerView,你可以扩展SwipeRefreshLayout并覆盖canChildScrollUp()方法,并将检查逻辑放在那里。

EDIT2: RecyclerView 已发布,目前无需使用此修复程序。

【讨论】:

  • 我已更新代码以修复适配器为 getCount() 返回 0 的情况
  • 我觉得应该是:if (direction &lt; 0)
  • 所以你是说canScrollVertically 根本没有在任何地方调用?那么你可能仍在使用 RecyclerView 的库版本。在构造函数中添加一些Log,看看它是否被调用。
  • 我看到了和@Marky17一样的东西,方法没有被调用。挖掘后,原因是我的 FixedRecyclerView (FRV) 不是 SwipeRefreshLayout (SRL) 的直接后代。我的 FRV 在 FrameLayout 内(所以我可以添加一个 QuickReturn 栏)。所以,当 SRL 试图问它的孩子“CanScrollVertically?”它询问的是封闭的 FrameLayout,而不是 FRV。如果 FRV 是(唯一?)SRL 的直接子代,则您的方法非常有效。对于我的应用程序,我调整了 SRL 以检查 FRV 子节点是否遇到 FrameLayout。谢谢!
  • 只是补充一点,如果您的 recyclerview 在顶部添加了填充以快速返回工具栏模式,那么我们需要通过将 getChildAt(0).getTop()
【解决方案3】:

我最近遇到了同样的问题。我尝试了@Krunal_Patel 建议的方法,但它在我的 Nexus 4 中大部分时间都有效,而在三星 Galaxy s2 中根本不起作用。在调试时,recyclerView.getChildAt(0).getTop() 对于 RecyclerView 总是不正确的。所以,经过各种方法,我想我们可以利用LayoutManager的findFirstCompletelyVisibleItemPosition()方法来预测RecyclerView的第一项是否可见,来开启SwipeRefreshLayout。代码如下。希望它可以帮助尝试解决相同问题的人。干杯。

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

【讨论】:

  • 虽然我不同意使用滚动监听器,但我使用了相同的条件覆盖了SwipeRefreshLayoutcanChildScrollUp 方法,它似乎运行良好。
  • AppCompat 21.0.2 版在我的情况下不再需要任何解决方法
  • @darnmason 很高兴知道:)
  • @darnmason 我尝试使用 21.0.2。但对我来说还是一样。它没有用。
  • linearLayoutManager.findFirstCompletelyVisibleItemPosition() 总是为我返回零!
【解决方案4】:

这就是我解决此问题的方法。对于最终在这里搜索类似解决方案的其他人来说,这可能很有用。

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

我希望这绝对可以帮助正在寻找类似解决方案的人。

【讨论】:

  • 如果没有项目完全可见,这将失败。使用这个int firstPos=linearLayoutManager.findFirstVisibleItemPosition(); if (firstPos&gt;0) { refreshLayout.setEnabled(false); } else { if(linearLayoutManager.findFirstCompletelyVisibleItemPosition()&gt;0) refreshLayout.setEnabled(true); }
【解决方案5】:

源代码 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);
    }
});

【讨论】:

  • 请考虑使用某种开源文件托管服务(例如 GitHub 或 BitBucket),而不是压缩整个项目并将其上传到 Google Drive。
【解决方案6】:

没有一个答案对我有用,但我设法通过自定义实现 LinearLayoutManager 来实现自己的解决方案。在这里发布以防其他人需要。

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

然后,你只需调用

recyclerView?.layoutManager = LayoutManagerScrollFixed(requireContext())

它正在工作!

【讨论】:

    【解决方案7】:

    不幸的是,这是 LinearLayoutManager 中的一个已知错误。当第一个项目可见时,它不会正确计算ScrollOffset。 发布后会修复。

    【讨论】:

    • 这似乎没有修复
    • 我创建了一个示例应用程序并且工作正常。您可以在 b.android.com 上创建示例应用程序和错误报告吗?谢谢。
    • 我的布局非常复杂,在尝试在示例应用程序中重现该问题时,我意识到 AppCompat 的次要版本 21.0.2 已修复该问题。 :D
    • 这是个好消息,感谢您的检查。我强烈建议您更新您的主项目,因为还有其他错误修复。
    【解决方案8】:

    我也遇到过同样的问题。我通过添加滚动侦听器来解决它,该侦听器将等到在RecyclerView 上绘制预期的第一个可见项目。您也可以绑定其他滚动侦听器,沿着这个。在您使用标题视图持有者的情况下应启用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);
                }
            }
        }
    

    用法:

    SwipeRefreshLayoutToggleScrollListener listener = new SwipeRefreshLayoutToggleScrollListener(mSwiperRefreshLayout);
    listener.addScrollListener(this); //optional
    listener.addScrollListener(mScrollListener1); //optional
    mRecyclerView.setOnScrollLIstener(listener);
    

    【讨论】:

      【解决方案9】:

      我遇到了同样的问题。我的解决方案是覆盖OnScrollListeneronScrolled 方法。

      解决方法在这里:

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

      【讨论】:

        【解决方案10】:

        这是处理这个问题的一种方法,它也处理 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);
            }
          }
        

        【讨论】:

          【解决方案11】:

          krunal 的解决方案很好,但它的工作方式类似于 hotfix,并且不涵盖某些特定情况,例如这个:

          假设 RecyclerView 在屏幕中间包含一个 EditText。我们启动应用程序 (topRowVerticalPosition = 0),点击 EditText。结果,软键盘出现,RecyclerView 的大小减小,系统自动滚动以保持EditText 可见,topRowVerticalPosition 不应该为0,但不会调用onScrolled,也不会重新计算topRowVerticalPosition。

          因此,我建议这个解决方案:

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

          将内部RecyclerView指定给SupportSwipeRefreshLayout后,如果RecyclerView不能向上滚动,它会自动发送触摸事件给SupportSwipeRefreshLayout,否则发送给RecyclerView。

          【讨论】:

            【解决方案12】:

            单线解决方案。

            setOnScrollListener 已弃用。

            您可以将 setOnScrollChangeListener 用于同样的目的:

            recylerView.setOnScrollChangeListener((view, i, i1, i2, i3) -> swipeToRefreshLayout.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0));
            

            【讨论】:

            • setOnScrollChangeListener 要求的最低 API 级别为 23
            【解决方案13】:

            如果有人发现这个问题并且对答案不满意:

            SwipeRefreshLayout 似乎与具有 1 个以上项目类型的适配器不兼容。

            【讨论】:

            • 在我们的案例中,对于 2 种项目类型没有任何问题,至少对于 androidX。
            猜你喜欢
            • 2015-10-31
            • 2017-01-02
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2023-03-11
            相关资源
            最近更新 更多