RecyclerView sans fin avec ProgressBar pour la pagination

J’utilise un RecyclerView et récupère des objects d’une API par lots de dix. Pour la pagination, j’utilise EndlessRecyclerOnScrollListener .

Tout fonctionne correctement. Il ne rest plus qu’à append une progression au bas de la liste pendant que le lot d’objects suivant est récupéré par l’API. Voici une capture d’écran de l’application Google Play Store, montrant un ProgressBar dans ce qui est sûrement un RecyclerView :

entrer la description de l'image ici

Le problème est que ni RecyclerView ni EndlessRecyclerOnScrollListener prennent en charge l’affichage d’un ProgressBar en bas lorsque le prochain lot d’objects est extrait.

J’ai déjà vu les réponses suivantes:

1. Placez un ProgressBar indéterminé comme pied de page dans une grid RecyclerView .

2. Ajout d’éléments à Endless Scroll RecyclerView avec ProgressBar en bas .

Je ne suis pas satisfait de ces réponses (toutes deux par la même personne). Cela implique de placer un object null dans le jeu de données à mi-chemin pendant que l’utilisateur fait défiler et de le retirer après la livraison du lot suivant. Cela ressemble à un piratage qui évite le problème principal qui peut ou peut ne pas fonctionner correctement. Et cela provoque un peu de discorde et de distorsion dans la liste

Utiliser SwipeRefreshLayout n’est pas une solution ici. SwipeRefreshLayout implique de tirer depuis le haut pour récupérer les éléments les plus récents , sans afficher de vue de progression.

Quelqu’un peut-il s’il vous plaît fournir une bonne solution pour cela? Je suis intéressé de savoir comment Google a mis en place ceci pour ses propres applications (l’application Gmail l’a aussi). Y a-t-il des articles où cela est montré en détail? Toutes les réponses et commentaires seront appréciés. Je vous remercie.

Quelques autres références :

1. Pagination avec RecyclerView . (Superbe aperçu …)

2. En- tête et pied de page RecyclerView . (Plus du même …)

3. RecyclerView sans fin avec ProgressBar en bas .

Je l’ai implémenté sur mon ancien projet, je l’ai fait comme suit …

J’ai créé une interface comme les gars de vos exemples l’ont fait

 public interface LoadMoreItems { void LoadItems(); } 

Ensuite, j’ajoute un addOnScrollListener() sur mon Adapter

  recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); totalItemCount = linearLayoutManager.getItemCount(); lastVisibleItem = linearLayoutManager .findLastVisibleItemPosition(); if (!loading && totalItemCount <= (lastVisibleItem + visibleThreshold)) { //End of the items if (onLoadMoreListener != null) { onLoadMoreListener.LoadItems(); } loading = true; } } }); 

Le onCreateViewHolder() est l'endroit où je place le ProgressBar ou non.

 @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { RecyclerView.ViewHolder vh; if (viewType == VIEW_ITEM) { View v = LayoutInflater.from(parent.getContext()).inflate( R.layout.list_row, parent, false); vh = new StudentViewHolder(v); } else { View v = LayoutInflater.from(parent.getContext()).inflate( R.layout.progressbar_item, parent, false); vh = new ProgressViewHolder(v); } return vh; } 

Sur ma MainActivity c'est là que je mets le LoadItems() pour append les autres éléments:

 mAdapter.setOnLoadMoreListener(new LoadMoreItems() { @Override public void LoadItems() { DataItemsList.add(null); mAdapter.notifyItemInserted(DataItemsList.size() - 1); handler.postDelayed(new Runnable() { @Override public void run() { // remove progress item DataItemsList.remove(DataItemsList.size() - 1); mAdapter.notifyItemRemoved(DataItemsList.size()); //add items one by one //When you've added the items call the setLoaded() mAdapter.setLoaded(); //if you put all of the items at once call // mAdapter.notifyDataSetChanged(); } }, 2000); //time 2 seconds } }); 

Pour plus d'informations, je viens de suivre ce Github repository ( Note: ceci utilise AsyncTask peut-être que c'est utile comme réponse, puisque je l'ai fait manuellement pas avec les données de l' API mais ça devrait aussi bien fonctionner ) -avec-barre de progression

Aussi, je ne sais pas si vous l'avez nommé, mais j'ai également trouvé ce post infinite_scrolling_recyclerview , peut-être que cela pourrait aussi vous aider.

Si ce n'est pas ce que vous cherchez, faites le moi savoir et dites-moi ce qui ne va pas avec ce code et je vais essayer de le modifier comme vous le souhaitez.

J'espère que cela aide.

MODIFIER

Comme vous ne voulez pas supprimer un object ... J'ai trouvé un gars qui supprime le footer uniquement sur ce post: diseño-android-endless-recyclerview .

Ceci est pour ListView mais je sais que vous pouvez l'adapter à RecyclerView il ne supprime aucun élément qu'il met simplement en regard de Visible/Invisible ProgressBar detecting-end-of-listview

Jetez également un coup d'oeil à: cette question android-implementing-progressbar-and-loading-for-endless-list-like-android

Il y a une autre façon de faire cela.

  • En premier lieu, getItemCount de votre adaptateur renvoie listItems.size() + 1
  • retourne VIEW_TYPE_LOADING dans getItemViewType() pour la position >= listItems.size() . De cette manière, le chargeur ne sera affiché qu’à la fin de la liste des vues du recycleur. Le seul problème avec cette solution est que, même après avoir atteint la dernière page, le chargeur sera affiché, donc pour corriger cela, vous stockez le x-pagination-total-count dans l’adaptateur, et
  • puis vous changez la condition pour retourner le type de vue à

    (position >= listItem.size())&&(listItem.size <= xPaginationTotalCount) .

Je viens de trouver cette idée maintenant que pensez-vous?

VOICI UNE APPROCHE PLUS SIMPLE ET PLUS PROPRE.

Implémentez le défilement sans fin à partir de ce guide Codepath , puis suivez les étapes suivantes.

1. Ajouter une barre de progression sous RecyclerView.

     

Ici Android: paddingBottom = “50dp” et Android: clipToPadding = “false” sont très importants.

2. Obtenez une référence à la barre de progression.

 progressBar = findViewById(R.id.progressBar); 

3. Définissez les méthodes pour afficher et masquer la barre de progression.

 void showProgressView() { progressBar.setVisibility(View.VISIBLE); } void hideProgressView() { progressBar.setVisibility(View.INVISIBLE); } 

J’aime l’idée d’append un support de vue de progression à un adaptateur, mais cela a tendance à mener à une manipulation logique moche pour obtenir ce que vous voulez. L’approche du titulaire de la vue vous oblige à vous protéger de l’élément de pied de page supplémentaire en agitant avec les valeurs de retour de getItemCount() , getItemViewType() , getItemId(position) et tout autre type de getItem(position) à inclure.

Une autre approche consiste à gérer la visibilité ProgressBar au niveau des Fragment ou des Activity en affichant ou en masquant la barre de ProgressBar sous RecyclerView lorsque le chargement commence et se termine respectivement. Cela peut être réalisé en incluant la ProgressBar directement dans la présentation de la vue ou en l’ajoutant à une classe RecyclerView ViewGroup personnalisée. Cette solution entraînera généralement moins de maintenance et moins de bogues.

MISE À JOUR: Ma suggestion pose un problème lorsque vous faites défiler la vue pendant le chargement du contenu. Le ProgressBar va coller au bas de la disposition de la vue. Ce n’est probablement pas le comportement que vous voulez. Pour cette raison, l’ajout d’un support de vue de progression à votre adaptateur est probablement la meilleure solution fonctionnelle. N’oubliez pas de garder vos méthodes d’accesseur d’élément. 🙂

voici ma solution sans append de faux article (dans Kotlin mais simple):

dans votre adaptateur, ajoutez:

 private var isLoading = false private val VIEWTYPE_FORECAST = 1 private val VIEWTYPE_PROGRESS = 2 override fun getItemCount(): Int { if (isLoading) return items.size + 1 else return items.size } override fun getItemViewType(position: Int): Int { if (position == items.size - 1 && isLoading) return VIEWTYPE_PROGRESS else return VIEWTYPE_FORECAST } override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder { if (viewType == VIEWTYPE_FORECAST) return ForecastHolder(LayoutInflater.from(context).inflate(R.layout.item_forecast, parent, false)) else return ProgressHolder(LayoutInflater.from(context).inflate(R.layout.item_progress, parent, false)) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) { if (holder is ForecastHolder) { //init your item } } public fun showProgress() { isLoading = true } public fun hideProgress() { isLoading = false } 

maintenant, vous pouvez facilement appeler showProgress() avant l’appel de l’API. et hideProgress() après l’appel de l’API.

Cette solution est inspirée de la solution Akshar Patels sur cette page. Je l’ai un peu modifié.

  1. Lors du chargement des premiers éléments, il est intéressant de centrer le ProgressBar.

  2. Je n’ai pas aimé le rest du remplissage vide en bas quand il n’y avait plus d’éléments à charger. Cela a été supprimé avec cette solution.

Tout d’abord le XML:

    

Ensuite, j’ai ajouté les éléments suivants par programmation.

Lorsque les premiers résultats ont été chargés, ajoutez ceci à votre onScrollListener. Il déplace la barre de progression de centre en bas:

 ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) loadingVideos.getLayoutParams(); layoutParams.topToTop = ConstraintLayout.LayoutParams.UNSET; loadingVideos.setLayoutParams(layoutParams); 

S’il n’y a plus d’éléments, retirez le remplissage en bas comme ceci:

 recyclerView.setPadding(0,0,0,0); 

Cachez et affichez votre ProgressBar comme d’habitude.

Une approche différente consisterait à lancer l’appel de l’API dans onBindViewHolder et à placer initialement les éléments dans un indicateur de progression. Une fois l’appel terminé, vous mettez à jour la vue (masquer la progression et afficher les données reçues). Par exemple, avec Picasso pour le chargement d’images, la méthode onBindViewHolder ressemblerait à ceci

 @Override public void onBindViewHolder(final MovieViewHolder holder, final int position) { final Movie movie = items.get(position); holder.imageProgress.setVisibility(View.VISIBLE); Picasso.with(context) .load(NetworkingUtils.getMovieImageUrl(movie.getPosterPath())) .into(holder.movieThumbImage, new Callback() { @Override public void onSuccess() { holder.imageProgress.setVisibility(View.GONE); } @Override public void onError() { } }); } 

À mon avis, il y a deux cas qui peuvent apparaître:

  1. où vous téléchargez tous les éléments en version légère avec un appel (par exemple, l’adaptateur sait immédiatement qu’il devra traiter 40 images, mais le télécharge à la demande -> cas que j’ai montré précédemment avec Picasso)
  2. où vous travaillez avec un chargement paresseux réel et vous demandez au serveur de vous donner des morceaux de données supplémentaires. Dans ce cas, la première condition préalable est d’avoir une réponse adéquate du serveur avec les informations nécessaires. Fore exemple {“offset”: 0, “total”: 100, “items”: [{items}]}

La réponse signifie que vous avez reçu le premier bloc de 100 données au total. Mon approche serait quelque chose comme ceci:

Affichage Après avoir obtenu le premier bloc de données (par exemple 10), ajoutez-les à l’adaptateur.

RecyclerView.Adapter.getItemCount Tant que le nombre actuel d’éléments disponibles est inférieur au montant total (par exemple, disponible 10; total 100), dans la méthode getItemCount, vous retournerez items.size () + 1

RecyclerView.Adapter.getItemViewType si la quantité totale de données est supérieure à la quantité d’éléments disponibles dans l’adaptateur et à la position = items.size () (c.-à-d. Que vous avez ajouté de manière fictive un élément dans la méthode getItemCount), vous retournez un indicateur de progression . Sinon, vous retournerez le type de disposition normal

RecyclerView.Adapter.onCreateViewHolder Lorsque vous êtes invité à utiliser le type de vue d’indicateur de progression, il vous suffit de demander à votre présentateur d’obtenir des blocs supplémentaires et de mettre à jour l’adaptateur.

Donc, en gros, c’est une approche où vous n’avez pas à append / supprimer des éléments de la liste et où vous avez le contrôle de la situation lorsque le chargement paresseux sera déclenché.

Voici l’exemple de code:

 public class ForecastListAdapter extends RecyclerView.Adapter { private final Context context; private List items; private ILazyLoading lazyLoadingListener; public static final int VIEW_TYPE_FIRST = 0; public static final int VIEW_TYPE_REST = 1; public static final int VIEW_TYPE_PROGRESS = 2; public static final int totalItemsCount = 14; public ForecastListAdapter(List items, Context context, ILazyLoading lazyLoadingListener) { this.items = items; this.context = context; this.lazyLoadingListener = lazyLoadingListener; } public void addItems(List additionalItems){ this.items.addAll(additionalItems); notifyDataSetChanged(); } @Override public int getItemViewType(int position) { if(totalItemsCount > items.size() && position == items.size()){ return VIEW_TYPE_PROGRESS; } switch (position){ case VIEW_TYPE_FIRST: return VIEW_TYPE_FIRST; default: return VIEW_TYPE_REST; } } @Override public ForecastVH onCreateViewHolder(ViewGroup parent, int viewType) { View v; switch (viewType){ case VIEW_TYPE_PROGRESS: v = LayoutInflater.from(parent.getContext()).inflate(R.layout.forecast_list_item_progress, parent, false); if (lazyLoadingListener != null) { lazyLoadingListener.getAdditionalItems(); } break; case VIEW_TYPE_FIRST: v = LayoutInflater.from(parent.getContext()).inflate(R.layout.forecast_list_item_first, parent, false); break; default: v = LayoutInflater.from(parent.getContext()).inflate(R.layout.forecast_list_item_rest, parent, false); break; } return new ForecastVH(v); } @Override public void onBindViewHolder(ForecastVH holder, int position) { if(position < items.size()){ Forecast item = items.get(position); holder.date.setText(FormattingUtils.formatTimeStamp(item.getDt())); holder.minTemperature.setText(FormattingUtils.getRoundedTemperature(item.getTemp().getMin())); holder.maxTemperature.setText(FormattingUtils.getRoundedTemperature(item.getTemp().getMax())); } } @Override public long getItemId(int position) { long i = super.getItemId(position); return i; } @Override public int getItemCount() { if (items == null) { return 0; } if(items.size() < totalItemsCount){ return items.size() + 1; }else{ return items.size(); } } public class ForecastVH extends RecyclerView.ViewHolder{ @BindView(R.id.forecast_date)TextView date; @BindView(R.id.min_temperature)TextView minTemperature; @BindView(R.id.max_temperature) TextView maxTemperature; public ForecastVH(View itemView) { super(itemView); ButterKnife.bind(this, itemView); } } public interface ILazyLoading{ public void getAdditionalItems(); }} 

Peut-être que cela vous incitera à faire quelque chose qui conviendra à vos besoins