RecyclerView sans fin avec ProgressBar pour la pagination
j'utilise un RecyclerView
et je récupère des objets D'une API par lots de dix. Pour la pagination, j'utilise EndlessRecyclerOnScrollListener
.
tout fonctionne correctement. Il ne reste plus qu'à ajouter un spinner progress en bas de la liste pendant que le prochain lot d'objets 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
:
le problème est que ni le RecyclerView
ni le EndlessRecyclerOnScrollListener
n'ont de support intégré pour afficher un ProgressBar
en bas pendant que le lot suivant d'objets est récupéré.
j'ai déjà vu les réponses suivantes:
1. mettez une indéterminée ProgressBar
comme pied de page dans un RecyclerView
grille .
2. ajout d'articles au rouleau sans fin RecyclerView
avec ProgressBar
au bas .
Je ne suis pas satisfait de ces réponses (les deux par la même personne). Il s'agit de placer un objet null
dans le jeu de données à mi-chemin pendant que l'utilisateur fait défiler, puis de le sortir après la livraison du prochain lot. Il on dirait un piratage qui évite le problème principal qui peut ou ne peut pas fonctionner correctement. Et elle provoque un peu de secousses et de distorsion dans la liste
utilisant SwipeRefreshLayout
n'est pas une solution ici. SwipeRefreshLayout
implique de tirer du haut pour aller chercher le plus récent articles, et il ne montre pas une vue de progrès de toute façon.
Quelqu'un peut-il s'il vous plaît fournir une bonne solution pour cela? Je suis intéressé savoir comment Google a mis en place pour leurs propres applications (l'application Gmail a trop). Existe-il des articles où cela est montré dans le détail? Toutes les réponses et commentaires seront appréciés. Remercier.
quelques autres références :
1. Pagination avec RecyclerView
. (Superbe vue d'ensemble ...)
2. RecyclerView
en-tête et pied de page . (Plus de la même chose ...)
3. sans fin RecyclerView
avec ProgressBar
au fond .
7 réponses
j'ai mis en œuvre ceci sur mon ancien projet, je l'ai fait comme suit...
j'ai créé un interface
comme les gars de vos exemples l'ont fait
public interface LoadMoreItems {
void LoadItems();
}
puis 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 où je mets 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 mon MainActivity
c'est là que j'ai mis le LoadItems()
pour ajouter les autres éléments est:
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 ma réponse, puisque je l'ai fait manuellement pas avec les données de API
mais il devrait fonctionner aussi ) aussi ce post m'a été utile sans fin-recyclerview-with-progress-bar
aussi je ne sais pas si vous nommé mais j'ai également trouvé ce post infinite_scrolling_recyclerview , peut-être qu'il pourrait également 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 j'essaierai de le modifier comme vous voulez.
J'espère que ça aidera.
EDIT
Puisque vous ne voulez pas supprimer un élément... J'ai trouvé je suppose qu'un gars qui enlève le footer
seulement sur ce post : diseño-android-sans fin-recyclerview .
c'est pour ListView
mais je sais que vous pouvez l'adapter à RecyclerView
il ne supprime pas tout ce qu'il met Visible/Invisible
le ProgressBar
regardez : detecting-end-of-listview
regardez aussi : cette question android-implementing-progressbar-and-loading-for-endless-list-like-android
VOICI UNE APPROCHE PLUS SIMPLE ET PLUS PROPRE.
mettre en œuvre le défilement sans fin à partir de ce guide Codepath et ensuite suivre les étapes suivantes.
1. Ajouter la barre de progression sous le RecyclerView.
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_movie_grid"
android:layout_width="0dp"
android:layout_height="0dp"
android:paddingBottom="50dp"
android:clipToPadding="false"
android:background="@android:color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</android.support.v7.widget.RecyclerView>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
android:background="@android:color/transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
ici android: paddingBottom= "50dp" et android:clipToPadding= "false" sont très important.
2. Obtenir une référence à la barre de progression.
progressBar = findViewById(R.id.progressBar);
3. Définir des méthodes pour afficher et masquer la barre de progression.
void showProgressView() {
progressBar.setVisibility(View.VISIBLE);
}
void hideProgressView() {
progressBar.setVisibility(View.INVISIBLE);
}
Il y a une autre façon de le faire.
- tout D'abord le getItemCount de votre adaptateur retourne
listItems.size() + 1
- retour
VIEW_TYPE_LOADING
dansgetItemViewType()
pourposition >= listItems.size()
. De cette façon, le chargeur ne sera affiché qu'à la fin de la liste des recycleurs. Le seul problème avec cette solution est même après avoir atteint la dernière page, le chargeur sera affiché, afin de corriger que vous stockez lex-pagination-total-count
dans l'adaptateur, et -
alors vous changez la condition pour retourner le type de vue en
(position >= listItem.size())&&(listItem.size <= xPaginationTotalCount)
.
je viens d'avoir cette idée maintenant que pensez-vous?
j'aime l'idée d'ajouter un support de vue de progression à un adaptateur, mais il tend à conduire à une manipulation logique moche pour obtenir ce que vous voulez. L'approche view holder vous oblige à vous prémunir contre le pied de page supplémentaire en jouant avec les valeurs de retour de getItemCount()
, getItemViewType()
, getItemId(position)
et toute sorte de méthode getItem(position)
que vous pouvez vouloir inclure.
une autre approche consiste à gérer la visibilité ProgressBar
au Fragment
." ou Activity
en montrant ou en masquant le ProgressBar
en dessous du RecyclerView
lorsque le chargement commence et se termine respectivement. Cela peut être réalisé en incluant le ProgressBar
directement dans la disposition de la vue ou en l'ajoutant à une classe personnalisée RecyclerView
ViewGroup
. 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 vers le haut pendant que le contenu est chargé. Le ProgressBar
coller au bas de la mise en page de la vue. Ce n'est probablement pas le comportement que vous souhaitez. 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'accesseurs d'articles. :)
voici ma solution sans ajouter un faux article (en Kotlin mais simple):
dans votre adaptateur ajouter:
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 API. et hideProgress()
après appel API a été fait.
cette solution est inspirée de Akshar Patels solution sur cette page. Je l'ai modifiée un peu.
-
lors du chargement des premiers éléments, il semble agréable d'avoir la barre de progression centrée.
-
Je n'ai pas aimé le rembourrage vide restant au fond quand il n'y avait plus d'articles à charger. Cela a été supprimé avec cette solution.
Première le XML:
<android.support.v7.widget.RecyclerView android:id="@+id/video_list" android:paddingBottom="60dp" android:clipToPadding="false" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> </android.support.v7.widget.RecyclerView> <ProgressBar android:id="@+id/progressBar2" style="?android:attr/progressBarStyle" android:layout_marginTop="10dp" android:layout_width="wrap_content" android:layout_centerHorizontal="true" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/>
puis j'ai ajouté la suivante programmatiquement.
lorsque les premiers résultats ont été chargés, ajoutez ceci à votre onScrollListener. Il déplace la barre de progression du centre vers le bas:
ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) loadingVideos.getLayoutParams();
layoutParams.topToTop = ConstraintLayout.LayoutParams.UNSET;
loadingVideos.setLayoutParams(layoutParams);
quand il n'y a plus d'articles, enlevez le rembourrage au bas comme ceci:
recyclerView.setPadding(0,0,0,0);
cacher et montrer votre ProgressBar comme d'habitude.
une approche différente consisterait à lancer l'appel API à l'intérieur de onBindViewHolder et à placer d'abord dans items view some progress indicator. Après l'appel est terminé, vous mettez à jour la vue (cacher la progression et l'affichage des données reçues). Par exemple avec Picasso pour le chargement d'image, 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() {
}
});
}
selon moi, il y a deux cas qui peuvent apparaître:
- où vous téléchargez tous les éléments en lumière version avec un seul appel (par exemple l'adaptateur sait immédiatement qu'il devra traiter avec 40 images, mais le télécharge sur demande - > cas que J'ai montré précédemment avec Picasso)
- où vous travaillez avec le chargement vraiment paresseux et vous demandez au serveur de vous donner un morceau supplémentaire de données. Dans ce cas, la première condition est d'avoir une réponse adéquate du serveur avec les informations nécessaires. Par exemple { "offset": 0, "total": 100, "articles": [{articles}] }
la réponse signifie que vous avez reçu la première tranche de 100 données. Mon approche serait quelque chose comme ceci:
View Après avoir obtenu le premier morceau de données (par exemple 10), ajoutez-les dans l'adaptateur.
RecyclerView.Adaptateur.getItemCount Tant que le montant actuel des articles disponibles est inférieur au montant total (par exemple disponible 10; total 100), dans la méthode getItemCount vous retournerez les articles.Taille() + 1
RecyclerView.Adaptateur.getItemViewType si la quantité totale de données est supérieure à la quantité d'articles disponibles dans Adaptateur et la position = articles.size() (c'est à dire que vous avez fictively article supplémentaire dans getItemCount méthode), comme le type de vue que vous revenir quelques progrès-indicateur. Sinon, vous retournerez le type de disposition normal
RecyclerView.Adaptateur.onCreateViewHolder Quand on vous demande d'utiliser progress-vue d'indicateur Tapez, tout ce que vous avez à faire est de demander à votre présentateur d'obtenir un morceau supplémentaire d'articles et de mettre à jour l'adaptateur
donc fondamentalement, c'est l'approche où vous n'avez pas à Ajouter/Supprimer des éléments de la liste et où vous avez le contrôle sur la situation lorsque le chargement paresseux sera déclenché.
Voici l'exemple de code:
public class ForecastListAdapter extends RecyclerView.Adapter<ForecastListAdapter.ForecastVH> {
private final Context context;
private List<Forecast> 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<Forecast> items, Context context, ILazyLoading lazyLoadingListener) {
this.items = items;
this.context = context;
this.lazyLoadingListener = lazyLoadingListener;
}
public void addItems(List<Forecast> 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 inspirera pour faire quelque chose qui répondra à vos besoins