RecyclerView и SwipeRefreshLayout
Я использую новый RecyclerView-Layout
в SwipeRefreshLayout
и испытываю странное поведение. При прокрутке списка вверх, иногда вид сверху.
![List scrolled to the top]()
Если я попытаюсь прокрутить вверх до конца - триггеры Pull-To-Refresh.
![cutted row]()
Если я попытаюсь удалить Shipe-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 вида в этом списке).
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
.
Если кто-то еще испытал это поведение и решил его или, по крайней мере, нашел причину этого?
Ответы
Ответ 1
Прежде чем использовать это решение:
RecyclerView еще не завершен, НЕ ИСПОЛЬЗУЙТЕ ЭТО В ПРОИЗВОДСТВЕ, ЕСЛИ ВЫ НЕ МЕНЯ!
Как и в ноябре 2014 года, в RecyclerView все еще есть ошибки, из-за которых canScrollVertically
возвращает ложно преждевременно. Это решение решит все проблемы прокрутки.
Падение раствора:
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), а затем возвращается.
EDIT:
И если вы по какой-то причине не хотите расширять RecyclerView
, вы можете расширить SwipeRefreshLayout
и переопределить метод canChildScrollUp()
и поместить туда проверочную логику.
EDIT2:
RecyclerView был выпущен, и до сих пор нет необходимости использовать это исправление.
Ответ 2
напишите следующий код в 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);
}
});
Ответ 3
Недавно я столкнулся с той же проблемой. Я пробовал подход, предложенный @Krunal_Patel, но он работал большую часть времени в моем Nexus 4 и вообще не работал в галактике s2 ssung. При отладке 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);
}
});
Ответ 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);
}
}
});
Я надеюсь, что это может определенно помочь кому-то, кто ищет подобное решение.
Ответ 5
Исходный код
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);
}
});
Ответ 6
К сожалению, это известная ошибка в LinearLayoutManager. Он не правильно вычисляетScrollOffset, когда первый элемент виден.
будет исправлено, когда оно будет выпущено.
Ответ 7
У меня возникла такая же проблема. Я решил это, добавив слушателя прокрутки, который будет ждать, пока ожидаемый первый видимый элемент не будет нарисован на 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);
Ответ 8
Я сталкиваюсь с той же проблемой. Мое решение переопределяет метод 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);
}
}
});
Ответ 9
Здесь один из способов справиться с этим, который также обрабатывает 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);
}
}
Ответ 10
Решение 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, она автоматически отправит событие touch в SupportSwipeRefreshLayout, если RecyclerView не сможет прокручиваться и в RecyclerView иначе.
Ответ 11
Однолинейное решение.
setOnScrollListener устарела.
Вы можете использовать setOnScrollChangeListener для той же цели, как это:
recylerView.setOnScrollChangeListener((view, i, i1, i2, i3) -> swipeToRefreshLayout.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0));
Ответ 12
В случае, если кто-то найдет этот вопрос и не будет удовлетворен ответом:
Похоже, что SwipeRefreshLayout не совместим с адаптерами, которые имеют более 1 типа элемента.