Бесконечный RecyclerView с ProgressBar для разбиения на страницы

Я использую RecyclerView и выбор объектов из API в пакетном режиме по десять. Для нумерации страниц я использую EndlessRecyclerOnScrollListener,

Все работает правильно. Теперь осталось только добавить индикатор выполнения внизу списка, в то время как следующий пакет объектов выбирается API. Вот скриншот приложения Google Play Store, показывающий ProgressBar в том, что, безусловно, RecyclerView:

введите описание изображения здесь

Проблема в том, что ни RecyclerView ни EndlessRecyclerOnScrollListener иметь встроенную поддержку для отображения ProgressBar внизу, пока выбирается следующая партия объектов.

Я уже видел следующие ответы:

1. поставить неопределенный ProgressBar как нижний колонтитул в RecyclerView сетка

2. Добавление предметов в бесконечный свиток RecyclerView с ProgressBar внизу

Я не удовлетворен этими ответами (оба от одного и того же человека). Это включает в себя null Объект в наборе данных на полпути, пока пользователь прокручивает, а затем вынимает его после доставки следующей партии. Это похоже на взлом, который обходит основную проблему, которая может или не может работать должным образом. И это приводит к небольшому шуму и искажению в списке

С помощью SwipeRefreshLayout это не решение здесь. SwipeRefreshLayout включает в себя перетаскивание сверху, чтобы получить новейшие предметы, и это не показывает прогресс прогресса в любом случае.

Может ли кто-нибудь предоставить хорошее решение для этого? Мне интересно знать, как Google реализовал это для своих собственных приложений (в приложении Gmail это тоже есть). Есть ли статьи, где это подробно показано? Все ответы и комментарии будут оценены. Спасибо.

Некоторые другие ссылки:

1. Нумерация страниц с RecyclerView, (Превосходный обзор...)

2. RecyclerView верхний и нижний колонтитулы. (Больше того же самого...)

3. Бесконечный RecyclerView с ProgressBar внизу

11 ответов

ЗДЕСЬ ПРОСТО И ЧИСТЫЙ ПОДХОД.

Реализуйте бесконечную прокрутку из этого руководства по Codepath, а затем выполните следующие шаги.

1. Добавьте индикатор выполнения под 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" />

Здесь очень важны android:paddingBottom="50dp" и android:clipToPadding="false".

2. Получить ссылку на индикатор выполнения.

progressBar = findViewById(R.id.progressBar);

3. Определите методы, чтобы показать и скрыть индикатор выполнения.

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

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

Я реализовал это на моем старом проекте, я сделал это следующим образом...

Я создал interface как ребята из ваших примеров сделали

public interface LoadMoreItems {
  void LoadItems();
}

Затем я добавил добавил addOnScrollListener() на моем 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;

                }
            }
        });

onCreateViewHolder() где я положил ProgressBar или нет.

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

На моем MainActivity вот где я положил LoadItems() добавить другие элементы это:

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

        }
    });

Для получения дополнительной информации я просто следовал этому Github repository (Примечание: это использует AsyncTask может быть, это полезно в качестве ответа, так как я сделал это вручную, а не с данными из API но это должно работать так же) также этот пост был полезен для меня http://android-pratap.blogspot.com.es/2015/06/endless-recyclerview-with-progress-bar.html

Также я не знаю, назвали ли вы его, но я также нашел этот пост http://msobhy.me/2015/09/05/infinite_scrolling_recyclerview/, возможно, он также может помочь вам.

Если это не то, что вы ищете, дайте мне знать и скажите, что не так с этим кодом, и я постараюсь изменить его по своему усмотрению.

Надеюсь, поможет.

РЕДАКТИРОВАТЬ

Так как вы не хотите удалять элемент... Я обнаружил, что один парень, который удаляет footer только в этом посте: http://danielme.com/2015/09/13/diseno-android-endless-recyclerview/.

Это для ListView но я знаю, что вы можете адаптировать его к RecyclerView он не удаляет какой-либо элемент, он просто кладет Visible/Invisible ProgressBar взглянуть: detecting-end-of-listview

Также посмотрите на этот вопрос android-implementing-progressbar-and-loading-for-endless-list-like-android

Есть еще один способ сделать это.

  • Сначала возвращается ваш адаптер getItemCount listItems.size() + 1
  • вернуть VIEW_TYPE_LOADING в getItemViewType() за position >= listItems.size(), Таким образом, загрузчик будет показан только в конце списка просмотра переработчика. Единственная проблема, связанная с этим решением, заключается в том, что даже после достижения последней страницы будет показан загрузчик, поэтому для исправления необходимо сохранить x-pagination-total-count в адаптере, и
  • затем вы меняете условие, чтобы вернуть тип просмотра

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

Я только что пришел с этой идеей, что ты думаешь?

Вот мой обходной путь без добавления поддельного элемента (в Kotlin, но простой):

в свой адаптер добавьте:

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
}

теперь вы можете легко позвонить showProgress() до вызова API. а также hideProgress() после вызова API

Мне нравится идея добавить держатель переходов в адаптер, но это приводит к некрасивым логическим манипуляциям, чтобы получить то, что вы хотите. Подход держателя вида заставляет вас защититься от дополнительного элемента нижнего колонтитула, перебирая возвращаемые значения getItemCount(), getItemViewType(), getItemId(position) и любой вид getItem(position) метод, который вы можете включить.

Альтернативный подход заключается в управлении ProgressBar видимость на Fragment или же Activity уровень, показывая или скрывая ProgressBar ниже RecyclerView когда загрузка начинается и заканчивается соответственно. Это может быть достигнуто путем включения ProgressBar непосредственно в макете представления или добавив его в пользовательский RecyclerViewViewGroup учебный класс. Такое решение обычно приводит к меньшему количеству обслуживания и меньшему количеству ошибок.

ОБНОВЛЕНИЕ: Мое предложение создает проблему, когда вы прокручиваете представление вверх во время загрузки контента. ProgressBar будет придерживаться нижней части макета представления. Это, вероятно, не то поведение, которое вы хотите. По этой причине, добавление держателя просмотра прогресса к вашему адаптеру, вероятно, является лучшим, функциональным решением. Только не забывайте охранять свои методы доступа к элементам.:)

Другое возможное решение - использовать ConcatAdapter, доступный в RecyclerView 1.2.0. Недостатком является то, что эта версия библиотеки еще находится в альфа-версии.

При таком подходе для индикатора выполнения используется отдельный адаптер, связанный с основным адаптером.

       val concatAdapter = ConcatAdapter(dataAdapter, progressAdapter)

progressAdapter должен возвращать 0 или 1 из метода getItemCount (), в зависимости от состояния загрузки.

Подробнее: https://medium.com/androiddevelopers/merge-adapters-sequential-with-mergeadapter-294d2942127a

И проверьте текущую стабильную версию библиотеки recyclerview, которая может быть уже в стабильной версии на момент чтения: https://developer.android.com/jetpack/androidx/releases/recyclerview


Другим жизнеспособным подходом было бы использование украшений элементов представления ресайклера. Использование этого подхода также избавит от изменения ViewHolders. Также возможна анимация декоратора, см. Например: https://medium.com/mobile-app-development-publication/animating-recycler-view-decorator-9b15fa4b2c23

По сути, декоратор элементов добавляется, когда индикатор загрузки должен присутствовать с функцией recyclerView.addItemDecoration(), а затем удаляться с помощью recyclerView.removeItemDecoration(). Постоянно invalidateItemDecorations() в recyclerView, пока отображается украшение элемента для запуска анимации.


Третья возможность - использовать библиотеку пейджинга от Google (часть Jetpack), адаптеры верхнего и нижнего колонтитула, доступные в версии 3 (все еще в бета-версии)https://www.youtube.com/watch?v=1cwqGOku2a4

Это решение вдохновлено решением Akshar Patels на этой странице. Я немного изменил это.

  1. При загрузке первых элементов выглядит хорошо, что ProgressBar центрирован.

  2. Мне не нравились оставшиеся пустые отступы внизу, когда больше не было элементов для загрузки. Это было удалено с этим решением.

Сначала 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"/>

Затем я добавил следующее программно.

Когда будут загружены первые результаты, добавьте это в свой onScrollListener. Он перемещает ProgressBar из центра вниз:

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

Когда больше нет элементов, удалите отступы внизу, как это:

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

Скрыть и показать свой ProgressBar как обычно.

Попробуйте этот простой код:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerview_cities"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_above="@id/preogressbar"
    />
<ProgressBar
    android:id="@+id/preogressbar"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="gone"
    android:layout_alignParentBottom="true"
    android:layout_centerHorizontal="true"
    />
</RelativeLayout>

Сделайте индикатор выполнения видимым, когда элементы вашего списка уже прокручены, и скройте, когда вы получите данные из своей службы.

Вы можете использовать тег layout_above в recycleView следующим образом:

<RelativeLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_marginLeft="16dp"
                    android:layout_marginRight="16dp"
                    android:orientation="vertical">
                    <androidx.recyclerview.widget.RecyclerView
                        android:id="@+id/rv"
                        android:layout_below="@+id/tv2"
                        android:layout_width="match_parent"
                        android:focusable="false"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="24dp"
                        android:layout_above="@+id/pb_pagination"/>

                     <ProgressBar
                        android:id="@+id/pb_pagination"
                        style="@style/Widget.AppCompat.ProgressBar"
                        android:layout_width="30dp"
                        android:layout_height="30dp"
                        android:indeterminate="true"
                         android:visibility="gone"
                        android:layout_alignParentBottom="true"
                        android:layout_centerHorizontal="true" />
    </RelativeLayout>

Добавить в адаптер

// Вставляем новый элемент в RecyclerView в предопределенную позицию public void insert(int position, JobModel data) {joblist.add(position, data);notifyItemInserted (позиция); }.

public void updateList (данные ArrayList) {попробуйте {для (int i = joblist.size(); i <data.size (); i ++) insert (i, data.get (i)); }catch (Исключение e) {}}.

// вызов из активности, когда page==2apiSaveJobsAdapter.updateList(joblist);

Другой подход заключается в запуске вызова API внутри onBindViewHolder и первоначальном размещении в элементах просмотра некоторого индикатора прогресса. После завершения вызова вы обновляете представление (скрываете прогресс и отображаете полученные данные). Например, при использовании Picasso для загрузки изображений метод onBindViewHolder будет выглядеть следующим образом

@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() {

                }
            });
}

На мой взгляд, есть два случая, которые могут появиться:

  1. где вы загружаете все элементы в облегченной версии одним вызовом (например, адаптер сразу знает, что ему придется иметь дело с 40 фотографиями, но загружает его по требованию -> случай, который я показал ранее с Пикассо)
  2. где вы работаете с реальной отложенной загрузкой и просите сервер предоставить вам дополнительную порцию данных. В этом случае первым условием является получение адекватного ответа от сервера с необходимой информацией. Пример для примера { "offset": 0, "total": 100, "items": [{items}] }

Там ответ означает, что вы получили первый кусок из 100 данных. Мой подход будет примерно таким:

Просмотр После получения первого куска данных (например, 10) добавьте их в адаптер.

RecyclerView.Adapter.getItemCount Пока текущее количество доступных элементов меньше, чем общее количество (например, доступно 10; всего 100), в методе getItemCount вы будете возвращать items.size () + 1

RecyclerView.Adapter.getItemViewType, если общий объем данных превышает количество доступных элементов в адаптере и position = items.size() (т.е. вы фиктивно добавили элемент в метод getItemCount), в качестве типа представления вы возвращаете некоторый индикатор прогресса, В противном случае вы вернете нормальный тип макета

RecyclerView.Adapter.onCreateViewHolder Когда вас попросят использовать тип представления индикатора прогресса, все, что вам нужно сделать, это попросить вашего докладчика получить дополнительный кусок элементов и обновить адаптер

По сути, это подход, при котором вам не нужно добавлять / удалять элементы из списка, и когда вы контролируете ситуацию, когда будет запущена отложенная загрузка.

Вот пример кода:

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

Может быть, это вдохновит вас на создание чего-то, что будет соответствовать вашим потребностям