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 типа просмотра в этом списке).

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.

Если кто-нибудь еще испытал такое поведение и решил его или, по крайней мере, нашел его причину?


person Lukas Olsen    schedule 07.08.2014    source источник
comment
У меня была точно такая же проблема. Далее я заметил, что RecyclerView всегда возвращает 0 из getScrollY (). Возможно, это логично, так как разные LayoutManager можно менять местами, а не прокручивать. Я не уверен, потому что я больше не исследовал. Я рад найти решение, но проголосую за ответ, когда смогу протестировать его после отпуска через две недели.   -  person cybergen    schedule 14.08.2014
comment
Текстурированный Лукас Олсен выглядит невероятно.   -  person Jared Burrows    schedule 29.10.2014


Ответы (12)


Прежде чем использовать это решение: 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 выпущен, и пока нет необходимости использовать это исправление.

person tom91136    schedule 10.08.2014
comment
Я обновил код, чтобы исправить случай, когда адаптер возвращает 0 для getCount (). - person tom91136; 11.08.2014
comment
Думаю, должно быть: if (direction < 0) - person cybergen; 03.09.2014
comment
У меня это не работает. Этот метод даже не вызывается, если список уже отображает центр данных в списке. Когда я опускаю его, он просто обновляется. - person Markymark; 28.12.2014
comment
То есть вы говорите, что canScrollVertically вообще нигде не вызывается? Тогда вы все еще можете использовать библиотечную версию RecyclerView. Добавьте Log в конструктор и посмотрите, вызывается ли он вообще. - person tom91136; 28.12.2014
comment
Я видел то же, что и @ Marky17, метод не вызывался. После раскопок причина заключалась в том, что мой FixedRecyclerView (FRV) не был ПРЯМЫМ потомком SwipeRefreshLayout (SRL). Мой FRV находился внутри FrameLayout (поэтому я мог добавить панель QuickReturn). Итак, когда SRL попытался спросить своего дочернего элемента CanScrollVertical? он запрашивал включающий FrameLayout, а не FRV. Ваш подход отлично работает, если FRV является (единственным?) Прямым потомком SRL. Для своего приложения я настроил SRL, чтобы проверять наличие дочернего элемента FRV, если он встречает FrameLayout. Спасибо! - person MojoTosh; 02.01.2015
comment
Это сработало для меня: return getChildAt (0) .getTop ()! = 0 - person user1354603; 23.01.2015
comment
Чтобы добавить к этому, если в вашем recyclerview добавлено дополнение вверху для шаблона панели инструментов быстрого возврата, нам нужно учесть это, изменив getChildAt (0) .getTop () ‹0 на getChildAt (0) .getTop ()‹ getPaddingTop () - person Steve G.; 03.02.2015
comment
canScrollVertical (направление) было именно тем, что я искал - person Penzzz; 11.06.2015

напишите следующий код в 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);
        }
    });
person krunal patel    schedule 07.08.2014
comment
Я использую recyclerview, а не listview - person Lukas Olsen; 08.08.2014
comment
просто чтобы немного расширить ответ: если вы устанавливаете отступ для своих элементов с помощью addItemDecoration, а также имеете верхний отступ на самом ReyclerView, вы должны переключить свой SwipeRefreshLayout следующим образом: boolean canEnableSwipeRefresh = mLayoutManager.findFirstVisibleItemPosition() == 0 && recyclerView.getChildAt(0).getTop() - (offset + mRecView.getPaddingTop()) == 0; где offset, если верхнее заполнение ваших элементов - person Droidman; 10.12.2014
comment
@krunalpatel Ваши параметры onScrollStateChanged неверны. Они должны быть: onScrollStateChanged (RecyclerView recyclerView, int scrollState) - person IgorGanapolsky; 07.01.2015
comment
recyclerView.setOnScrollListener устарело, используйте вместо него addOnScrollListener - person jiawen; 11.11.2015

Недавно я столкнулся с той же проблемой. Я попробовал подход, предложенный @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);
        }
    });
person balachandarkm    schedule 02.10.2014
comment
Хотя я не согласен с использованием прослушивателя прокрутки, я использовал то же условие, перекрывая метод SwipeRefreshLayout canChildScrollUp, и, похоже, он работает хорошо. - person darnmason; 26.11.2014
comment
Версия 21.0.2 AppCompat устранила необходимость в каком-либо обходном пути в моем случае - person darnmason; 08.02.2015
comment
@darnmason Полезно знать :) - person balachandarkm; 08.02.2015
comment
@darnmason Я пробовал с 21.0.2. Но для меня все так же. Это не сработало. - person balachandarkm; 25.02.2015
comment
linearLayoutManager.findFirstCompletelyVisibleItemPosition() всегда возвращает мне ноль! - person Saravanabalagi Ramachandran; 09.04.2016
comment
Это не удастся, если ни один элемент полностью не виден. Используйте это int firstPos=linearLayoutManager.findFirstVisibleItemPosition(); if (firstPos>0) { refreshLayout.setEnabled(false); } else { if(linearLayoutManager.findFirstCompletelyVisibleItemPosition()>0) refreshLayout.setEnabled(true); } - person jatin rana; 02.12.2018

Вот как я решил эту проблему в моем случае. Это может быть полезно для кого-то еще, кто окажется здесь для поиска решений, подобных этому.

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

Я надеюсь, что это определенно поможет тем, кто ищет подобное решение.

person Amrut Bidri    schedule 07.04.2015
comment
Это не удастся, если ни один элемент полностью не виден. Используйте это int firstPos=linearLayoutManager.findFirstVisibleItemPosition(); if (firstPos>0) { refreshLayout.setEnabled(false); } else { if(linearLayoutManager.findFirstCompletelyVisibleItemPosition()>0) refreshLayout.setEnabled(true); } - person jatin rana; 02.12.2018

Исходный код 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);
    }
});
person Keshav Gera    schedule 02.09.2017
comment
Пожалуйста, подумайте об использовании какой-либо службы хостинга файлов с открытым исходным кодом (например, GitHub или BitBucket) вместо того, чтобы заархивировать весь ваш проект и загрузить его на Google Диск. - person Edric; 10.12.2018

К сожалению, это известная ошибка в LinearLayoutManager. Он не выполняет правильное вычислениеScrollOffset, когда виден первый элемент. будет исправлен, когда он будет выпущен.

person yigit    schedule 08.08.2014
comment
Кажется, это не было исправлено - person darnmason; 26.11.2014
comment
Я создал образец приложения и отлично работает. Можете ли вы создать образец приложения и отчет об ошибке на b.android.com? Спасибо. - person yigit; 01.12.2014
comment
Мой макет довольно сложен, и, пытаясь воспроизвести проблему в образце приложения, я понял, что есть второстепенный выпуск 21.0.2 AppCompat, в котором это исправлено. : D - person darnmason; 02.12.2014
comment
Это отличные новости, спасибо за проверку. Я настоятельно рекомендую обновить и ваш основной проект, потому что есть и другие исправления ошибок. - person yigit; 02.12.2014

У меня такая же проблема. Я решил это, добавив прослушиватель прокрутки, который будет ждать, пока ожидаемый первый видимый элемент не отобразится на 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);
person Nikola Despotoski    schedule 27.11.2014

У меня та же проблема. Мое решение переопределяет 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);
        }
    }
});
person SilentKnight    schedule 10.06.2015

Вот один из способов справиться с этим, который также обрабатывает 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);
    }
  }
person android developer    schedule 28.11.2015

Решение 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 в противном случае.

person alcsan    schedule 07.03.2016

Однолинейное решение.

setOnScrollListener больше не поддерживается.

Вы можете использовать setOnScrollChangeListener для той же цели, например:

recylerView.setOnScrollChangeListener((view, i, i1, i2, i3) -> swipeToRefreshLayout.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0));
person SANAT    schedule 02.04.2018
comment
setOnScrollChangeListener требует минимального уровня API как 23 - person Parth Anjaria; 28.04.2018

Если кто-то нашел этот вопрос и не удовлетворился ответом:

Кажется, что SwipeRefreshLayout несовместим с адаптерами, у которых более 1 типа элементов.

person Elynad    schedule 11.06.2019
comment
в нашем случае нет проблем с двумя типами элементов, по крайней мере, с androidX. - person goodhyun; 06.02.2020