Endless RecyclerView с ProgressBar для разбивки на страницы

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

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

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

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

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

1. Поместите неопределенный ProgressBar в нижний колонтитул в RecyclerView сетку. p >

2. Добавление элементов в Endless Scroll RecyclerView с ProgressBar внизу. p >

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

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

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

Другие ссылки:

1. Разбиение страницы с помощью RecyclerView. (Превосходный обзор...)

2. RecyclerView заголовок и нижний колонтитул. (Больше того же...)

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

Ответы

Ответ 1

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

Реализуйте бесконечную прокрутку из этого руководства по 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);
}

Ответ 2

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

Я создал 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 но это должно работать также), также этот пост был полезен для меня endless-recyclerviewview -с-прогресс-бар

Кроме того, я не знаю, назвали ли вы его, но я также нашел этот пост infinite_scrolling_recyclerview, возможно, он также может помочь вам.

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

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

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

Так как вы не хотите удалять элемент... Я обнаружил, что один парень, который удаляет footer только в этом посте: disño-android-endless-recyclerview.

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

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

Ответ 3

Есть и другой способ сделать это.

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

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

Я только что придумал эту идею, что вы думаете?

Ответ 4

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

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

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

Ответ 5

вот мой обходной путь без добавления фальшивого предмета (в 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.

Ответ 6

Это решение вдохновлено решением 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 как обычно.

Ответ 7

Вы можете использовать тег 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>

Ответ 8

Другим подходом было бы запустить вызов 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() {

                }
            });
}

Как я вижу, возможны два случая:

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

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

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

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

RecyclerView.Adapter.getItemViewType если общий объем данных больше, чем количество доступных элементов в адаптере, а позиция = 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();
}}

Возможно, это вдохновит вас сделать что-то, что удовлетворит ваши потребности.