Skip to content

Instantly share code, notes, and snippets.

@amaksoft
Created October 9, 2017 16:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save amaksoft/7737efb44bbcecc8cc262eb3874f16c5 to your computer and use it in GitHub Desktop.
Save amaksoft/7737efb44bbcecc8cc262eb3874f16c5 to your computer and use it in GitHub Desktop.
Missing emptyVIew for RecyclerVIew
package com.github.amaksoft.recyclerviewtools.adapter;
import android.support.annotation.IdRes;
import android.support.annotation.LayoutRes;
import android.support.annotation.Nullable;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import ru.altarix.mpgu3.R;
/**
* Адаптер {@link RecyclerView} для отображения плейсхолдера при пустом списке.
* Является оберткой для обычного адаптера.
* Если список в {@link #originalAdapter} пуст, то показывается плейсхолдер, иначе сам список.
* Плейсхолдер отобразится только после первого вызова одного из notify- методов, если данных нет.
* Работа проверена на {@link GridLayoutManager} и {@link LinearLayoutManager}
*
* Created by gars and amak on 4/13/17.
*/
@SuppressWarnings({"unused", "WeakerAccess"})
public class PlaceholderRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private int placeholderViewType = Integer.MAX_VALUE;
private String message;
protected RecyclerView.Adapter originalAdapter;
@SuppressWarnings("WeakerAccess")
protected RecyclerView recyclerView;
@LayoutRes
final int placeholderViewId;
@IdRes
@SuppressWarnings("WeakerAccess")
final int errorTextViewId;
protected boolean notified;
private boolean prevEmpty;
@Nullable
private PlaceholderToggleListener placeholderToggleListener;
/**
* Создает плейсхолдер-адаптер с вьюхой по умолчанию {@link R.layout#empty_list_placeholder}
* @param originalAdapter адаптер, отображающий сам список
*/
public PlaceholderRecyclerViewAdapter(RecyclerView.Adapter originalAdapter) {
this(originalAdapter, R.layout.empty_list_placeholder, R.id.tvEmptyListMessage, false);
}
/**
* Создает плейсхолдер-адаптер с вьюхой по умолчанию {@link R.layout#empty_list_placeholder}
* @param originalAdapter адаптер, отображающий сам список
* @param showImmediately - отображать ли плейсхолдер сразу при подключении адаптера к
* {@link RecyclerView} (если false - покажет после первого вызова одного notify* методов)
*/
public PlaceholderRecyclerViewAdapter(RecyclerView.Adapter originalAdapter, boolean showImmediately) {
this(originalAdapter, R.layout.empty_list_placeholder, R.id.tvEmptyListMessage, showImmediately);
}
/**
* Создает плейсхолдер-адаптер с заданной вьюхой и стандартным id {@link TextView}, отображающего ошибку {@link R.id#tvEmptyListMessage}
* @param originalAdapter адаптер, отображающий сам список
* @param placeholderViewId - id лейаута плейсхолдера
*/
public PlaceholderRecyclerViewAdapter(RecyclerView.Adapter originalAdapter, @LayoutRes int placeholderViewId) {
this(originalAdapter, placeholderViewId, R.id.tvEmptyListMessage, false);
}
/**
* Создает плейсхолдер-адаптер с заданной вьюхой и стандартным id {@link TextView}, отображающего ошибку {@link R.id#tvEmptyListMessage}
* @param originalAdapter адаптер, отображающий сам список
* @param placeholderViewId - id лейаута плейсхолдера
* @param showImmediately - отображать ли плейсхолдер сразу при подключении адаптера к
* {@link RecyclerView} (если false - покажет после первого вызова одного notify* методов)
*/
public PlaceholderRecyclerViewAdapter(RecyclerView.Adapter originalAdapter, @LayoutRes int placeholderViewId, boolean showImmediately) {
this(originalAdapter, placeholderViewId, R.id.tvEmptyListMessage, showImmediately);
}
/**
* Создает плейсхолдер-адаптер с вьюхой по умолчанию
* @param originalAdapter адаптер, отображающий сам список
* @param placeholderViewId - id лейаута плейсхолдера
* @param errorTextViewId - id {@link TextView}, в котором будет отображено сообщение об ошибке.
* @param showImmediately - отображать ли плейсхолдер сразу при подключении адаптера к
* {@link RecyclerView} (если false - покажет после первого вызова одного notify* методов)
*/
public PlaceholderRecyclerViewAdapter(RecyclerView.Adapter originalAdapter, @LayoutRes int placeholderViewId, @IdRes int errorTextViewId, boolean showImmediately) {
this.originalAdapter = originalAdapter;
this.placeholderViewId = placeholderViewId;
this.errorTextViewId = errorTextViewId;
this.originalAdapter.registerAdapterDataObserver(originalAdapterObserver);
this.registerAdapterDataObserver(placeholderAdapterObserver);
this.notified = showImmediately;
}
/**
* Задает сообщение, отображаемое в плейсхолдере
*
* @param mMessage сообщение
*/
public void setMessage(String mMessage) {
this.message = mMessage;
}
/**
* Позволяет узнать viewType, соответствующий плейсхолдеру.
* @return viewType плейсхолдера
*/
public int getPlaceholderViewType() {
return placeholderViewType;
}
/**
* Позволяет при необходимости изменить {@link Integer}, соответствующий viewType. По умолчанию - {@link Integer#MAX_VALUE}
* @param placeholderViewType желаемый viewType плейсхолдера.
*/
public void setPlaceholderViewType(int placeholderViewType) {
this.placeholderViewType = placeholderViewType;
}
/**
* Задает модуль для подготовки {@link RecyclerView} к отображению плейсхолдера
* @param placeholderToggleListener имплементация интерфейса
*/
public void setPlaceholderToggleListener(@Nullable PlaceholderToggleListener placeholderToggleListener) {
this.placeholderToggleListener = placeholderToggleListener;
}
/**
* Возвращает текущий модуль для подготовки {@link RecyclerView} к отображению плейсхолдера
*/
@Nullable
public PlaceholderToggleListener getPlaceholderToggleListener() {
return placeholderToggleListener;
}
@Override
public int getItemViewType(int position) {
return isEmpty() ? placeholderViewType : originalAdapter.getItemViewType(position);
}
/**
* Обновляет состояние {@link RecyclerView}.
* Работает аналогично {@link #notifyDataSetChanged()}, вынесено в отдельный метод, т.к.
* {@link #notifyDataSetChanged()} является final и не может быть переопределен в субклассах.
* @deprecated используйте обычные методы адаптера: {@link #notifyDataSetChanged()}, {@link #notifyItemInserted(int)} и прочие.
* Этот метод скоро будет удален.
*/
@Deprecated
public void notifyDataChanged() {
prepareRecyclerView(recyclerView);
notifyDataSetChanged();
}
/**
* Подготавливает {@link RecyclerView} к отображению плейсхолдера (переформатирует {@link android.support.v7.widget.RecyclerView.LayoutManager})
* @param recyclerView - {@link RecyclerView}, к которому применяем изменения
*/
@SuppressWarnings("WeakerAccess")
void prepareRecyclerView(RecyclerView recyclerView) {
notified = true;
if (isEmpty() ^ prevEmpty) { // Делаем перестройки RecyclerView только при смене состояния с пустого на заполненный и назад
if (placeholderToggleListener != null) {
placeholderToggleListener.onPlaceholderToggle(recyclerView, isEmpty());
}
}
prevEmpty = isEmpty();
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
this.recyclerView = recyclerView;
if (message == null) {
message = recyclerView.getContext().getString(R.string.no_data);
}
if (placeholderToggleListener == null && recyclerView.getLayoutManager() instanceof GridLayoutManager) {
placeholderToggleListener = GRID_LAYOUT_MANAGER_PREPARATION;
}
// если адаптер создается уже со списком то грид остается и количество колонок не меняется
boolean prevNotify = notified;
// поэтому дергаем это
prepareRecyclerView(recyclerView);
notified = prevNotify;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == placeholderViewType) {
return onCreateEmptyViewHolder(parent, viewType);
} else {
return originalAdapter.createViewHolder(parent, viewType);
}
}
@Override
public long getItemId(int position) {
if (isEmpty())
return super.getItemId(position);
return originalAdapter.getItemId(position);
}
/**
* Метод создает {@link RecyclerView.ViewHolder} с вьюхой плейсхолдера.
* При наследовании от этого класса можно передать свой (с обработкой кликов и пр. дополнениями)
*/
public RecyclerView.ViewHolder onCreateEmptyViewHolder(ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
View v = layoutInflater.inflate(placeholderViewId, parent, false);
RecyclerView.ViewHolder holder = new ViewHolder(v);
v.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
));
return holder;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (getItemViewType(position) == placeholderViewType) {
onBindEmptyViewHolder(holder, position);
} else {
//noinspection unchecked
originalAdapter.bindViewHolder(holder, position);
}
}
/**
* Метод подготавливает {@link RecyclerView.ViewHolder} с вьюхой плейсхолдера к отображению
* При наследовании от этого класса можно дополнить чем нужно
*/
@SuppressWarnings("WeakerAccess")
public void onBindEmptyViewHolder(RecyclerView.ViewHolder holder, int position) {
holder.itemView.setVisibility(notified ? View.VISIBLE : View.GONE);
((ViewHolder) holder).setMessage(message);
}
@Override
public int getItemCount() {
return isEmpty() ? 1 : originalAdapter.getItemCount();
}
class ViewHolder extends RecyclerView.ViewHolder {
private TextView tvMessage;
ViewHolder(View itemView) {
super(itemView);
View vMessage = itemView.findViewById(errorTextViewId);
if (vMessage instanceof TextView) tvMessage = (TextView) vMessage;
}
public void setMessage(String message) {
if (tvMessage != null) tvMessage.setText(message);
}
}
private boolean isEmpty() {
return originalAdapter.getItemCount() == 0;
}
/**
* Этот {@link RecyclerView.AdapterDataObserver} обрабатывает вызовы notify* методов, вызванных из
* оригинального адаптера и обновляет обертку (и тем самым {@link RecyclerView} т.к. он подписан на нее)
*/
@SuppressWarnings("FieldCanBeLocal")
protected RecyclerView.AdapterDataObserver originalAdapterObserver = new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
PlaceholderRecyclerViewAdapter.this.notifyDataSetChanged();
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
PlaceholderRecyclerViewAdapter.this.notifyItemRangeChanged(positionStart, itemCount);
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
PlaceholderRecyclerViewAdapter.this.notifyItemRangeChanged(positionStart, itemCount, payload);
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
PlaceholderRecyclerViewAdapter.this.notifyItemRangeInserted(positionStart, itemCount);
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
PlaceholderRecyclerViewAdapter.this.notifyItemRangeRemoved(positionStart, itemCount);
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
PlaceholderRecyclerViewAdapter.this.notifyDataSetChanged();
}
};
/**
* Этот {@link RecyclerView.AdapterDataObserver} обрабатывает вызовы notify* методов перед {@link RecyclerView}
* и выполняет все подготовительные операции.
*/
@SuppressWarnings("FieldCanBeLocal")
private RecyclerView.AdapterDataObserver placeholderAdapterObserver = new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
prepareRecyclerView(recyclerView);
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
prepareRecyclerView(recyclerView);
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
prepareRecyclerView(recyclerView);
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
prepareRecyclerView(recyclerView);
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
prepareRecyclerView(recyclerView);
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
prepareRecyclerView(recyclerView);
}
};
/**
* Интерфейс для кастомизации подготовки {@link RecyclerView} к отображению плейсхолдера на весь размер.
* Например для {@link GridLayoutManager} необходимо изменить SpanCount на 1, а затем вернуть назад при отображении списка.
* Или спрятать конфликтующие с плейсхолдером {@link android.support.v7.widget.RecyclerView.ItemDecoration}.
* Или выполнить любые другие операции, изначально не предусмотренные данным адаптером.
* @see #GRID_LAYOUT_MANAGER_PREPARATION пример использования в случае {@link GridLayoutManager}
*/
interface PlaceholderToggleListener {
/**
* Метод вызывается перед оповещением {@link RecyclerView} о смене состояния списка.
* Вызывается <b>только при смене состояния<b/> с заполненного (в оригинальном адаптере есть
* элементы) на пустое (элементов нет, нужно показать плейсхолдер) и обратно.
* @param recyclerView {@link RecyclerView}, который необходимо подготовить
* @param empty состояние адаптера: true - пустой, false - в списке есть элементы.
*/
void onPlaceholderToggle(RecyclerView recyclerView, boolean empty);
}
/**
* Подготавливает {@link RecyclerView} с {@link GridLayoutManager} для отображения плейсхолдера (меняет колчество столбцов на 1 и обратно)
*/
public static final PlaceholderToggleListener GRID_LAYOUT_MANAGER_PREPARATION = new PlaceholderToggleListener() {
private int originalGridSpanCount;
@Override
public void onPlaceholderToggle(RecyclerView recyclerView, boolean empty) {
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
GridLayoutManager gridLayoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
if (empty) {
originalGridSpanCount = gridLayoutManager.getSpanCount();
gridLayoutManager.setSpanCount(1);
} else {
gridLayoutManager.setSpanCount(originalGridSpanCount);
}
}
}
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment