RecyclerView和SwipeRefreshLayout

65

我正在使用新的RecyclerView-LayoutSwipeRefreshLayout中,遇到了一个奇怪的行为。当将列表向上滚动回到顶部时,有时顶部视图会被切断。

List scrolled to the top

如果现在尝试向上滚动 - 将触发下拉刷新。

cutted row

如果尝试移除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个Viewtypes)。

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

包含RecyclerSwipeRefreshLayoutFragment的代码。

如果有其他人遇到过这种行为并解决了它或者至少找到了原因,请告诉我?


我曾经遇到过完全相同的问题。此外,我注意到RecyclerView总是从getScrollY()返回0。也许这是合理的,因为可以交换各种LayoutManager,不一定是滚动的。我不确定,因为我没有进一步研究。我很高兴找到了解决方案,但在两周后度假后测试后,我会点赞该答案。 - cybergen
2
Lukas Olsen说:“纹理效果”看起来真是不可思议。 - Jared Burrows
14个回答

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

2
我正在使用RecyclerView,而不是ListView。 - Lukas Olsen
2
只是稍微扩展一下答案:如果您使用addItemDecoration为项目设置填充,并且在ReyclerView本身上有顶部填充,则可以按以下方式切换SwipeRefreshLayoutboolean canEnableSwipeRefresh = mLayoutManager.findFirstVisibleItemPosition() == 0 && recyclerView.getChildAt(0).getTop() - (offset + mRecView.getPaddingTop()) == 0;其中offset是您的项目的顶部填充。 - Droidman
@krunalpatel 您的onScrollStateChanged参数是错误的。正确格式应为:onScrollStateChanged(RecyclerView recyclerView, int scrollState)。 - IgorGanapolsky
recyclerView.setOnScrollListener已被弃用,请使用addOnScrollListener。 - jiawen

53

在使用此解决方案之前: 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() 方法,并在其中放置检查逻辑。

编辑2: RecyclerView 已经发布,到目前为止没有必要使用此修复方法。


1
我认为应该是:if (direction < 0) - cybergen
1
所以你是说canScrollVertically根本就没有被调用过?那么你可能还在使用RecyclerView的库版本。在构造函数中添加一些Log,看看它是否被调用了。 - tom91136
2
我看到了和@Marky17一样的问题,方法没有被调用。经过深入挖掘,原因是我的FixedRecyclerView(FRV)不是SwipeRefreshLayout(SRL)的直接子类。我的FRV在FrameLayout中(所以我可以添加QuickReturn栏)。因此,当SRL尝试询问其子项“CanScrollVertically?”时,它正在询问封闭的FrameLayout,而不是FRV。如果FRV是SRL的(唯一?)直接子项,则您的方法完美地运行。对于我的应用程序,我已经调整了SRL,以检查是否存在FRV子项,如果遇到FrameLayout。谢谢! - MojoTosh
1
补充一下,如果您的RecyclerView在顶部添加了填充以进行快速返回工具栏模式,那么我们需要通过将getChildAt(0)。getTop()<0更改为getChildAt(0)。getTop()< getPaddingTop()来进行调整。 - Steve G.
canScrollVertically(direction) 正是我正在寻找的。 - Penzzz
显示剩余3条评论

13

最近我遇到了同样的问题。我尝试了@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方法,看起来效果很好。 - darnmason
AppCompat的21.0.2版本在我的情况下消除了任何解决方法的需要。 - darnmason
@darnmason 好的,知道了 :) - balachandarkm
1
@darnmason 我尝试了21.0.2版本,但是对我来说还是一样的。它没有起作用。 - balachandarkm
linearLayoutManager.findFirstCompletelyVisibleItemPosition() 对于我总是返回零! - Saravanabalagi Ramachandran
如果没有任何项目完全可见,这将失败。使用以下代码:int firstPos = linearLayoutManager.findFirstVisibleItemPosition(); if (firstPos > 0) { refreshLayout.setEnabled(false); } else { if (linearLayoutManager.findFirstCompletelyVisibleItemPosition() > 0) refreshLayout.setEnabled(true); } - jatin rana

11

以下是我在解决这个问题时的方法。它可能对其他人有用,他们也在寻找类似的解决方案。

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 > 0) { refreshLayout.setEnabled(false); } else { if (linearLayoutManager.findFirstCompletelyVisibleItemPosition() > 0) refreshLayout.setEnabled(true); } - jatin rana

2

源代码 https://drive.google.com/open?id=0BzBKpZ4nzNzURkRGNVFtZXV1RWM

这是一个链接,它包含了相关的IT技术源代码。
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);
    }
});

2
请考虑使用某种开源文件托管服务(例如GitHub或BitBucket),而不是将整个项目压缩并上传到Google Drive。 - Edric

1
如果您正在使用RecyclerView而不是ScrollView,您可以执行以下操作,它将起作用。
        recyclerview.isNestedScrollingEnabled = true

1

对我来说,这些答案都不起作用,但是我成功地通过创建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())

它正在运行!



0

krunal的解决方案很好,但它像热修补一样工作,并且不能涵盖某些特定情况,例如:

假设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。


0

我也遇到了同样的问题。我通过添加滚动监听器来解决它,该监听器将等待直到预期的第一个可见项在 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);

0

我遇到了同样的问题。我的解决方案是重写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);
        }
    }
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接