RecyclerView и SwipeRefreshLayout
я использую новый RecyclerView-Layout
на SwipeRefreshLayout
и испытал странное поведение. При прокрутке списка назад к началу иногда вид сверху обрезается.
если я попытаюсь прокрутить вверх сейчас-триггеры Pull-To-Refresh.
если я попытаюсь удалить Swipe-Refresh-Layout вокруг Recycler-View, проблема исчезнет. И его можно воспроизводить на любом телефоне (не только устройства 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));
код Fragment
содержащий Recycler
и SwipeRefreshLayout
.
если кто-то еще испытал это поведение и решил его или, по крайней мере, нашел причину этого?
11 ответов:
перед использованием этого решения: RecyclerView еще не завершен, постарайтесь не использовать его в производстве, если вы не похожи на меня!Что касается ноября 2014 года, в 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 был выпущен и до сих пор нет необходимости использовать это исправление.
пишем следующий код в
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); } });
недавно я столкнулся с той же проблемой. Я попробовал подход, предложенный @Krunal_Patel, но он работал в большинстве случаев в моем Nexus 4 и вообще не работал в samsung galaxy s2. Во время отладки, recyclerView.getChildAt(0).getTop () всегда не подходит для RecyclerView. Итак, пройдя через различные методы, я понял, что мы можем использовать метод findFirstCompletelyVisibleItemPosition() LayoutManager, чтобы предсказать, виден ли первый элемент 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); } });
вот как я решил эту проблему в моем случае. Это может быть полезно для кого-то, кто в конечном итоге здесь для поиска решения подобных этому.
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); } } });
Я надеюсь, что это может помочь кому-то, кто ищет подобное решение.
Исходный Код https://drive.google.com/open?id=0BzBKpZ4nzNzURkRGNVFtZXV1RWM
recyclerView.setOnScrollListener(новый RecyclerView.OnScrollListener() {
public void onScrollStateChanged(RecyclerView recyclerView, int newState) { } public void onScrolled(RecyclerView recyclerView, int dx, int dy) { swipeRefresh.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0); } });
к сожалению, это известная ошибка в LinearLayoutManager. Он не computeScrollOffset правильно, когда первый элемент виден. будет исправлено, когда он будет выпущен.
Я испытал ту же проблему. Я решил это, добавив прослушиватель прокрутки, который будет ждать, пока ожидаемый первый видимый элемент не будет нарисован на
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);
я столкнулся с той же проблемой. Мое решение переопределяет
onScrolled
методOnScrollListener
.решение здесь:
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 хорошо, но оно работает как исправление и не охватывает некоторые конкретные случаи, например этот:
предположим, что 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, он автоматически отправит событие касания в SupportSwipeRefreshLayout, если RecyclerView нельзя прокрутить вверх и в RecyclerView в противном случае.