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 на этой странице. Я немного изменил это.
-
При загрузке первых элементов выглядит хорошо, что 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 как обычно.
Ответ 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();
}}
Возможно, это вдохновит вас сделать что-то, что удовлетворит ваши потребности.