63

我在 a 中使用新RecyclerView-LayoutSwipeRefreshLayout并经历了一种奇怪的行为。当将列表滚动回顶部时,有时顶部的视图会被切入。

列表滚动到顶部

如果我现在尝试滚动到顶部 - Pull-To-Refresh 触发器。

切割行

如果我尝试删除 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以及包含Recycler和的代码SwipeRefreshLayout

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

4

13 回答 13

75

addOnScrollListener将以下代码写入RecyclerView

像这样:

    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);
        }
    });
于 2014-08-07T13:21:24.397 回答
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()方法并将检查逻辑放在那里。

EDIT2: RecyclerView 已经发布,到目前为止没有必要使用这个修复。

于 2014-08-10T10:44:50.290 回答
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);
        }
    });
于 2014-10-02T11:53:01.130 回答
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);
            }
        }
    });

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

于 2015-04-07T06:46:06.577 回答
2

源代码 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);
    }
});
于 2017-09-02T10:19:14.777 回答
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())

它正在工作!

于 2021-12-08T19:17:45.230 回答
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);
于 2014-11-27T15:14:19.463 回答
0

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

于 2014-08-08T21:23:03.283 回答
0

我遇到了同样的问题。我的解决方案onScrolledOnScrollListener.

解决方法在这里:

    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);
        }
    }
});
于 2015-06-10T07:26:31.263 回答
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。

于 2016-03-07T15:11:27.897 回答
0

单线解决方案。

setOnScrollListener已弃用。

您可以将setOnScrollChangeListener用于相同的目的,如下所示:

recylerView.setOnScrollChangeListener((view, i, i1, i2, i3) -> swipeToRefreshLayout.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0));
于 2018-04-02T13:39:57.163 回答
0

这是处理此问题的一种方法,它也处理 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);
    }
  }
于 2015-11-28T23:24:14.843 回答
0

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

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

于 2019-06-11T14:17:14.000 回答