web-dev-qa-db-fra.com

RecyclerView et SwipeRefreshLayout

J'utilise le nouveau RecyclerView-Layout dans une SwipeRefreshLayout et j'ai eu un comportement étrange. Lorsque vous faites défiler la liste vers le haut, la vue du dessus est parfois masquée.

List scrolled to the top

Si j'essaie de faire défiler vers le haut maintenant - les déclencheurs Pull-To-Refresh. 

cutted row

Si j'essaie de supprimer la disposition de balayage dans la vue Recycler, le problème a disparu. Et sa reproductible sur n'importe quel téléphone (pas seulement les périphériques 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>

C’est ma disposition: les lignes sont créées dynamiquement par RecyclerViewAdapter (2 types de vues dans cette liste). 

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

Voici l'adaptateur. 

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

Et le code de la Fragment contenant la Recycler et la SwipeRefreshLayout

Si quelqu'un d'autre a vécu ce comportement et l'a résolu ou au moins en a trouvé la raison?

56
Lukas Olsen

Avant d’utiliser cette solution: RecyclerView n’est pas encore complet, N'ESSAYEZ PAS DE L’UTILISER DANS LA PRODUCTION SAUF SI VOUS ÊTES COMME MOI!

En ce qui concerne novembre 2014, il existe encore des bogues dans RecyclerView qui pourraient causer le renvoi prématuré de canScrollVertically. Cette solution résoudra tous les problèmes de défilement.

La solution en goutte:

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

    }
}

Vous n'avez même pas besoin de remplacer RecyclerView dans votre code par FixedRecyclerView, le remplacement de la balise XML serait suffisant! (Cela garantit que lorsque RecyclerView est terminé, la transition serait simple et rapide)

Explication:

Fondamentalement, canScrollVertically(boolean) renvoie faux trop tôt, nous vérifions donc si la variable RecyclerView est défilée jusqu'en haut de la première vue (où le haut du premier enfant serait 0), puis revient.

EDIT: .__ Et si vous ne voulez pas étendre RecyclerView pour une raison quelconque, vous pouvez étendre SwipeRefreshLayout, écraser la méthode canChildScrollUp() et y insérer la logique de vérification.

EDIT2: RecyclerView a été publié et jusqu’à présent, il n’était pas nécessaire d’utiliser ce correctif.

52
tom91136

écrire le code suivant dans addOnScrollListener de la RecyclerView

Comme ça:

    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

Je suis tombé sur le même problème récemment. J'ai essayé l'approche suggérée par @Krunal_Patel, mais cela a fonctionné la plupart du temps dans mon Nexus 4 et pas du tout dans samsung galaxy s2. Lors du débogage, recyclerView.getChildAt (0) .getTop () est toujours incorrect pour RecyclerView. Ainsi, après avoir étudié différentes méthodes, j'ai pensé que nous pouvions utiliser la méthode findFirstCompletelyVisibleItemPosition () de LayoutManager pour prédire si le premier élément de RecyclerView est visible ou non, pour activer SwipeRefreshLayout.Recherchez le code ci-dessous. J'espère que cela aidera quelqu'un qui essaie de résoudre le même problème. À votre santé. 

    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

Voici comment j'ai résolu ce problème dans mon cas. Cela pourrait être utile pour quelqu'un d'autre qui se retrouve ici pour chercher des solutions similaires à celle-ci.

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

J'espère que cela pourra certainement aider quelqu'un qui recherche une solution similaire.

10
Amrut Bidri

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

recyclerView.setOnScrollListener (nouveau 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

Si quelqu'un trouve cette question et n'est pas satisfait de la réponse:

Il semble que SwipeRefreshLayout n'est pas compatible avec les adaptateurs ayant plus d'un type d'élément.

0
Elynad

La solution de krunal est bonne, mais cela fonctionne comme un correctif et ne couvre pas certains cas spécifiques, par exemple celui-ci:

Supposons que le RecyclerView contient un EditText au milieu de l'écran. Nous commençons l'application (topRowVerticalPosition = 0), tapotez sur le EditText. En conséquence, le clavier logiciel apparaît, la taille de RecyclerView est réduite, le système le fait défiler automatiquement pour que EditText reste visible et topRowVerticalPosition ne doit pas être 0, mais onScrolled n'est pas appelé et topRowVerticalPosition n'est pas recalculé.

Par conséquent, je suggère cette solution:

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

Une fois que vous avez spécifié RecyclerView interne à SupportSwipeRefreshLayout, un événement tactile est automatiquement envoyé à SupportSwipeRefreshLayout s'il est impossible de faire défiler RecyclerView et à RecyclerView sinon.

0
alcsan

Voici un moyen de gérer cela, qui gère également 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);
    }
  }
0
android developer

malheureusement, il s'agit d'un bogue connu de LinearLayoutManager. Il ne compute pas correctementScrollOffset lorsque le premier élément est visible .

0
yigit

Je rencontre le même problème. Ma solution est de surcharger la méthode onScrolled de OnScrollListener.

La solution est ici:

    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

Solution simple ligne.

setOnScrollListener est obsolète.

Vous pouvez utiliser setOnScrollChangeListener pour le même but, comme ceci:

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

J'ai connu le même problème. Je l'ai résolu en ajoutant un écouteur de défilement qui attendra que le premier élément visible attendu soit dessiné sur la RecyclerView. Vous pouvez également lier d'autres auditeurs de défilement, le long de celui-ci. La première valeur visible attendue est ajoutée pour l'utiliser comme position de seuil lorsque la variable SwipeRefreshLayout doit être activée dans les cas où vous utilisez des détenteurs de vue d'en-tête.

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

Usage:

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