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 56

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

единое решение.

setOnScrollListener устарела.

можно использовать setOnScrollChangeListener для того же purspose, как это:

recylerView.setOnScrollChangeListener((view, i, i1, i2, i3) -> swipeToRefreshLayout.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0));