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 :

enter image description here

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 .

52
demandé sur Community 2015-12-23 11:47:09

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

15
répondu Skizo-ozᴉʞS 2017-05-23 12:34:41

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);
}
5
répondu Akshar Patel 2017-11-14 16:17:52

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 dans getItemViewType() pour position >= 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 le x-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?

4
répondu Bhargav 2016-01-12 11:43:51

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. :)

2
répondu Ryan 2016-04-06 23:07:14

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.

1
répondu Amir Ziarati 2018-04-27 09:27:28

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

  1. lors du chargement des premiers éléments, il semble agréable d'avoir la barre de progression centrée.

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

0
répondu John T 2018-06-21 10:06:29

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:

  1. 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)
  2. 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

-2
répondu Teodor Hirs 2016-10-22 15:06:24