webentwicklung-frage-antwort-db.com.de

RecyclerView und SwipeRefreshLayout

Ich verwende den neuen RecyclerView-Layout in einer SwipeRefreshLayout und erlebte ein seltsames Verhalten. Beim Zurückblättern der Liste nach oben wird manchmal die Ansicht oben eingeblendet.

List scrolled to the top

Wenn ich jetzt nach oben scrollen möchte, löst das Pull-To-Refresh aus. 

cutted row

Wenn ich versuche, das Swipe-Refresh-Layout um die Recycler-Ansicht zu entfernen, ist das Problem weg. Und es ist auf jedem Telefon (nicht nur L-Preview-Geräten) reproduzierbar. 

 <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>

Das ist mein Layout - die Zeilen werden vom RecyclerViewAdapter (2 Ansichtstypen in dieser Liste) dynamisch erstellt. 

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

Hier ist der Adapter. 

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

Und der Code der Fragment, der die Recycler und die SwipeRefreshLayout enthält. 

Wenn jemand anderes dieses Verhalten erlebt und gelöst hat oder zumindest den Grund dafür gefunden hat?

56
Lukas Olsen

Bevor Sie diese Lösung verwenden: RecyclerView ist noch nicht vollständig. VERSUCHEN SIE NICHT, SIE IN DER PRODUKTION ZU BENUTZEN, WENN SIE WIE MÖGEN!

Im November 2014 gibt es immer noch Fehler in RecyclerView, durch die canScrollVertically vorzeitig falsch ausgegeben wird. Diese Lösung löst alle Probleme beim Scrollen.

Der Tropfen in Lösung:

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

    }
}

Sie müssen nicht einmal RecyclerView in Ihrem Code durch FixedRecyclerView ersetzen. Das Ersetzen des XML-Tags wäre ausreichend! (Dies stellt sicher, dass der Übergang nach Abschluss von RecyclerView schnell und einfach ist.)

Erläuterung:

Grundsätzlich gibt canScrollVertically(boolean) zu früh false zurück. Daher prüfen wir, ob RecyclerView ganz bis zum Anfang der ersten Ansicht (wo der erste untergeordnete Wert 0 sein würde) gescrollt wird und kehren dann zurück.

EDIT: Wenn Sie RecyclerView aus irgendeinem Grund nicht erweitern möchten, können Sie SwipeRefreshLayout erweitern und die canChildScrollUp()-Methode überschreiben und die Prüflogik dort einfügen.

EDIT2: RecyclerView wurde veröffentlicht, und bis jetzt müssen Sie diesen Fix nicht verwenden.

52
tom91136

schreibe den folgenden Code in addOnScrollListener der RecyclerView

So was:

    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);
        }
    });
64
krunal patel

Ich bin kürzlich auf das gleiche Problem gestoßen. Ich habe den von @Krunal_Patel vorgeschlagenen Ansatz ausprobiert, aber er funktionierte meistens in meinem Nexus 4 und funktionierte überhaupt nicht in Samsung Galaxy S2. Beim Debuggen ist recyclerView.getChildAt (0) .getTop () für RecyclerView immer nicht korrekt. Nachdem ich verschiedene Methoden durchlaufen hatte, kam ich zu dem Schluss, dass wir die Methode findFirstCompletelyVisibleItemPosition () des LayoutManagers verwenden können, um vorherzusagen, ob das erste Element der RecyclerView sichtbar ist oder nicht, um SwipeRefreshLayout zu aktivieren. Hoffe, es hilft jemandem, der versucht, das gleiche Problem zu beheben. Prost. 

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

So habe ich dieses Problem in meinem Fall gelöst. Es könnte für jemanden nützlich sein, der hier nach ähnlichen Lösungen sucht.

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

Ich hoffe, das kann auf jeden Fall jemandem helfen, der nach einer ähnlichen Lösung sucht.

10
Amrut Bidri

Quellcodehttps://drive.google.com/open?id=0BzBKpZ4nzNzURkRGNVFtZXV1RWM

recyclerView.setOnScrollListener (neuer RecyclerView.OnScrollListener () {

    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
    }

    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        swipeRefresh.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0);
    }
});
1
Keshav Gera

Falls jemand diese Frage findet und mit der Antwort nicht zufrieden ist:

Es scheint, dass SwipeRefreshLayout nicht mit Adaptern kompatibel ist, die mehr als einen Elementtyp haben.

0
Elynad

Die Lösung von krunal ist gut, funktioniert jedoch als Hotfix und deckt nicht bestimmte Fälle ab, zum Beispiel den folgenden:

Angenommen, RecyclerView enthält einen EditText in der Mitte des Bildschirms. Wir starten die Anwendung (topRowVerticalPosition = 0) und tippen auf den EditText. Daraufhin wird die Softwaretastatur angezeigt, die Größe des RecyclerView wird verringert, es wird automatisch vom System gescrollt, um den EditText sichtbar zu halten, und topRowVerticalPosition sollte nicht 0 sein, aber onScrolled wird nicht aufgerufen und topRowVerticalPosition nicht neu berechnet.

Daher empfehle ich diese Lösung:

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

Nachdem Sie die interne RecyclerView für SupportSwipeRefreshLayout angegeben haben, wird das Touch-Ereignis automatisch an SupportSwipeRefreshLayout gesendet, wenn RecyclerView nicht nach oben gescrollt werden kann, und ansonsten zu RecyclerView.

0
alcsan

Hier haben Sie eine Möglichkeit, mit ListView/GridView umzugehen.

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

leider ist dies ein bekannter Fehler in LinearLayoutManager. ComputeScrollOffset wird nicht korrekt angezeigt, wenn das erste Element sichtbar ist.

0
yigit

Ich stoße auf das gleiche Problem. Meine Lösung überschreibt die onScrolled-Methode von OnScrollListener.

Umgehung ist hier:

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

Einzeilige Lösung.

setOnScrollListener ist veraltet.

Sie können setOnScrollChangeListener für denselben Zweck wie folgt verwenden:

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

Ich habe das gleiche Problem erlebt. Ich habe das Problem gelöst, indem ich einen Scroll-Listener hinzugefügt habe, der wartet, bis das erwartete erste sichtbare Element auf der Variable RecyclerView gezeichnet ist. Sie können auch andere Scroll-Listener binden. Der erwartete erste sichtbare Wert wird hinzugefügt, um ihn als Schwellenwertposition zu verwenden, wenn die SwipeRefreshLayout in Fällen aktiviert werden soll, in denen Sie die Header-Ansicht verwenden.

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

Verwendungszweck:

SwipeRefreshLayoutToggleScrollListener listener = new SwipeRefreshLayoutToggleScrollListener(mSwiperRefreshLayout);
listener.addScrollListener(this); //optional
listener.addScrollListener(mScrollListener1); //optional
mRecyclerView.setOnScrollLIstener(listener);
0