RecyclerView和SwipeRefreshLayout

我在SwipeRefreshLayout使用了新的RecyclerView-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> 

这是我的布局 – 行由RecyclerViewAdapterdynamic构build(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的代码。

如果有其他人经历过这种行为并解决了这个问题,或者至lessfind了原因呢?

在使用此解决scheme之前:RecyclerView尚未完成,请不要在生产中使用它,除非您像我一样!

至于2014年11月,RecyclerView中仍然存在一些bug,会导致canScrollVertically过早返回false。 该解决scheme将解决所有滚动问题。

解决scheme的下降:

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

您甚至不需要用FixedRecyclerViewreplace代码中的FixedRecyclerView ,replaceXML标签就足够了! (确保RecyclerView完成时,过渡将是快速和简单的)

说明:

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

编辑:如果你不想扩展RecyclerView由于某种原因,你可以扩展SwipeRefreshLayout并重写canChildScrollUp()方法,并把检查逻辑在那里。

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

将下面的代码写入RecyclerView addOnScrollListener

喜欢这个:

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

我最近遇到了同样的问题。 我尝试了@Krunal_Patel提出的方法,但它在我的Nexus 4中大部分时间都在工作,在三星Galaxy S2中根本不工作。 在debugging时,RecyclerView的recyclerView.getChildAt(0).getTop()总是不正确的。 所以,经过各种方法之后,我觉得我们可以利用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); } }); 

这就是我解决这个问题的方法。 对于那些最终在这里寻找类似的解决scheme的人来说,这可能是有用的。

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

我希望这肯定能帮助那些正在寻找类似解决scheme的人。

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

我遇到过同样的问题。 我通过添加滚动监听器来解决这个问题,这个监听器将等待直到在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); 

我遇到了同样的问题。 我的解决scheme是重写OnScrollListener onScrolled方法。

解决方法是这里:

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

这里有一个方法来处理这个,它也处理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); } } 

krunal的解决scheme是好的,但它的作用像修补程序,并不包括一些具体的情况,例如这一个:

假设RecyclerView在屏幕中间包含一个EditText。 我们启动应用程序(topRowVerticalPosition = 0),点击EditText。 因此,软件键盘显示,RecyclerView的大小减less,由系统自动滚动以保持EditText可见,并且topRowVerticalPosition不应该为0,但onScrolled不会被调用,topRowVerticalPosition不会被重新计算。

所以我build议这个解决scheme:

 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无法滚动,则会自动将Touch事件发送到SupportSwipeRefreshLayout,否则将自动将Touch事件发送到RecyclerView。

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