Бесконечный 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
непосредственно в макете представления или добавив его в пользовательский RecyclerView
ViewGroup
учебный класс. Такое решение обычно приводит к меньшему количеству обслуживания и меньшему количеству ошибок.
ОБНОВЛЕНИЕ: Мое предложение создает проблему, когда вы прокручиваете представление вверх во время загрузки контента. 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 на этой странице. Я немного изменил это.
При загрузке первых элементов выглядит хорошо, что ProgressBar центрирован.
Мне не нравились оставшиеся пустые отступы внизу, когда больше не было элементов для загрузки. Это было удалено с этим решением.
Сначала 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() {
}
});
}
На мой взгляд, есть два случая, которые могут появиться:
- где вы загружаете все элементы в облегченной версии одним вызовом (например, адаптер сразу знает, что ему придется иметь дело с 40 фотографиями, но загружает его по требованию -> случай, который я показал ранее с Пикассо)
- где вы работаете с реальной отложенной загрузкой и просите сервер предоставить вам дополнительную порцию данных. В этом случае первым условием является получение адекватного ответа от сервера с необходимой информацией. Пример для примера { "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();
}}
Может быть, это вдохновит вас на создание чего-то, что будет соответствовать вашим потребностям