package com.eyeem.recyclerviewtools.adapter;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.LruCache;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
/**
* Created by budius on 01.04.15.
* <p/>
* Base wrapping adapter that allows usage of headers, footers, sections and OnItemClick within a
* {@link android.support.v7.widget.RecyclerView RecyclerView}.
*/
public class WrapAdapter
extends RecyclerView.Adapter {
private static final int MAX_CACHE_SIZE = 666; // TODO: what's a sensible value?
private static final int MAIN_VIEW_TYPE_MASK = 0x40000000;
private static final long MAIN_ITEM_ID_MASK = 0x4000000000000000L;
private static final int HEADER_VIEW_TYPE_MASK = 0x1000000 + MAIN_VIEW_TYPE_MASK;
private static final long HEADER_ITEM_ID_MASK = 0x100000000000000L + MAIN_ITEM_ID_MASK;
private static final int FOOTER_VIEW_TYPE_MASK = 0x2000000 + MAIN_VIEW_TYPE_MASK;
private static final long FOOTER_ITEM_ID_MASK = 0x200000000000000L + MAIN_ITEM_ID_MASK;
private static final int SECTION_VIEW_TYPE_MASK = 0x4000000 + MAIN_VIEW_TYPE_MASK;
private static final long SECTION_ITEM_ID_MASK = 0x400000000000000L + MAIN_ITEM_ID_MASK;
static final int NOT_A_SECTION = -1;
protected final RecyclerView.Adapter wrapped;
private final AbstractSectionAdapter sections;
private OnItemClickListenerDetector onItemClickListenerDetector;
public WrapAdapter(RecyclerView.Adapter wrappedAdapter) {
this(wrappedAdapter, new EmptySectionAdapter());
}
public WrapAdapter(RecyclerView.Adapter wrappedAdapter, AbstractSectionAdapter sectionAdapter) {
if (wrappedAdapter == null || sectionAdapter == null)
throw new IllegalArgumentException("wrappedAdapter and sectionAdapter cannot be null");
this.wrapped = wrappedAdapter;
this.sections = sectionAdapter;
setHasStableIds(wrapped.hasStableIds());
wrapped.registerAdapterDataObserver(dataObserver);
// pre-init real item position
int count = wrapped.getItemCount();
int cacheSize = Math.min(count > 0 ? count : MAX_CACHE_SIZE / 2, MAX_CACHE_SIZE);
initPositionCaching(cacheSize);
}
// basic adapter callbacks (viewType, ID, count, create n bind viewHolder)
// ==============================================================================================
@Override public int getItemViewType(int position) {
if (isHeaderPosition(position)) {
return HEADER_VIEW_TYPE_MASK | position;
} else if (isFooterPosition(position)) {
return FOOTER_VIEW_TYPE_MASK | (getItemCount() - position - 1);
} else {
int sectionPosition = getSectionIndex(position);
if (sectionPosition != NOT_A_SECTION) {
return SECTION_VIEW_TYPE_MASK | sections.getSectionViewType(sectionPosition);
} else {
int type = wrapped.getItemViewType(recyclerToWrappedPosition.get(position));
if (type > MAIN_VIEW_TYPE_MASK)
throw new IllegalArgumentException("ItemView type cannot be greater than 0x" + Integer.toHexString(MAIN_VIEW_TYPE_MASK));
return type;
}
}
}
@Override public long getItemId(int position) {
if (!hasStableIds()) return RecyclerView.NO_ID;
if (isHeaderPosition(position)) {
return HEADER_ITEM_ID_MASK | position;
} else if (isFooterPosition(position)) {
return FOOTER_ITEM_ID_MASK | (getItemCount() - position);
} else {
int sectionPosition = getSectionIndex(position);
if (sectionPosition != NOT_A_SECTION) {
return SECTION_ITEM_ID_MASK | sections.getSectionId(sectionPosition);
} else {
long id = wrapped.getItemId(recyclerToWrappedPosition.get(position));
if (id > MAIN_ITEM_ID_MASK)
throw new IllegalArgumentException("ItemView type cannot be greater than 0x" + Long.toHexString(MAIN_ITEM_ID_MASK));
return id;
}
}
}
@Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
RecyclerView.ViewHolder viewHolder;
boolean extra = false;
if (isHeaderViewType(viewType)) {
extra = true;
viewHolder = new HeaderFooterHolder(getHeaders().get(removeMask(viewType, HEADER_VIEW_TYPE_MASK)));
} else if (isFooterViewType(viewType)) {
extra = true;
viewHolder = new HeaderFooterHolder(getFooters().get(removeMask(viewType, FOOTER_VIEW_TYPE_MASK)));
} else if (isSectionViewType(viewType)) {
extra = true;
viewHolder = sections.onCreateSectionViewHolder(parent, removeMask(viewType, SECTION_VIEW_TYPE_MASK));
} else {
viewHolder = wrapped.onCreateViewHolder(parent, viewType);
}
bindOnItemClickListener(viewHolder, extra);
return viewHolder;
}
@Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder instanceof HeaderFooterHolder)
return;
if (isSectionViewType(holder.getItemViewType())) {
sections.onBindSectionView(holder, getSectionIndex(position));
} else {
wrapped.onBindViewHolder(holder, recyclerToWrappedPosition.get(position));
}
}
@Override public int getItemCount() {
return wrapped.getItemCount() + sections.getSectionCount() + getHeaderCount() + getFooterCount();
}
// extended adapter callbacks, most adapters don't use, but just for completeness
// ==============================================================================================
@Override public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
if (holder instanceof HeaderFooterHolder || isSectionViewType(holder.getItemViewType()))
return;
wrapped.onViewAttachedToWindow(holder);
}
@Override public void onViewDetachedFromWindow(RecyclerView.ViewHolder holder) {
if (holder instanceof HeaderFooterHolder || isSectionViewType(holder.getItemViewType()))
return;
wrapped.onViewDetachedFromWindow(holder);
}
@Override public void onViewRecycled(RecyclerView.ViewHolder holder) {
if (holder instanceof HeaderFooterHolder || isSectionViewType(holder.getItemViewType()))
return;
wrapped.onViewRecycled(holder);
}
@Override public void onAttachedToRecyclerView(RecyclerView recyclerView) {
wrapped.onAttachedToRecyclerView(recyclerView);
}
@Override public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
wrapped.onDetachedFromRecyclerView(recyclerView);
}
@Override public boolean onFailedToRecycleView(RecyclerView.ViewHolder holder) {
if (holder instanceof HeaderFooterHolder || isSectionViewType(holder.getItemViewType()))
return super.onFailedToRecycleView(holder);
else
return wrapped.onFailedToRecycleView(holder);
}
// OnItemClick handling
// ==============================================================================================
/**
* Simple OnItemClick for RecyclerView. This binds a {@link android.view.View.OnClickListener}
* to the root view of the each view holder.
* <p/>
* This call automatically ignore clicks on headers, footers and sections.
*
* @param recyclerView the recyclerView this adapter will be attached to
* @param onItemClickListener the listener for the click events
*/
public void setOnItemClickListener(
RecyclerView recyclerView,
OnItemClickListenerDetector.OnItemClickListener onItemClickListener) {
setOnItemClickListener(recyclerView, onItemClickListener, true);
}
/**
* Simple OnItemClick for RecyclerView. This binds a {@link android.view.View.OnClickListener}
* to the root view of the each view holder.
*
* @param recyclerView the recyclerView this adapter will be attached to
* @param onItemClickListener the listener for the click events
* @param ignoreExtras true if it should ignore header, footer and sections; false otherwise
*/
public void setOnItemClickListener(
RecyclerView recyclerView,
OnItemClickListenerDetector.OnItemClickListener onItemClickListener,
boolean ignoreExtras) {
onItemClickListenerDetector = new OnItemClickListenerDetector(recyclerView, onItemClickListener, ignoreExtras);
}
private void bindOnItemClickListener(RecyclerView.ViewHolder holder, boolean isExtra) {
if (onItemClickListenerDetector == null) return;
if (onItemClickListenerDetector.ignoreExtras && isExtra)
return;
holder.itemView.setOnClickListener(onItemClickListenerDetector);
}
// View type helpers
// ==============================================================================================
private boolean isHeaderViewType(int viewType) {
return isViewType(viewType, HEADER_VIEW_TYPE_MASK);
}
private boolean isFooterViewType(int viewType) {
return isViewType(viewType, FOOTER_VIEW_TYPE_MASK);
}
private boolean isSectionViewType(int viewType) {
return isViewType(viewType, SECTION_VIEW_TYPE_MASK);
}
private boolean isViewType(int viewType, int viewTypeMask) {
return (viewTypeMask & viewType) == viewTypeMask;
}
private int removeMask(int val, int mask) {
return val & ~mask; // ~ is bitwise not: NOT mask AND val
}
// Headers and Footers
// ==============================================================================================
private List<View> headers; // Lazy initialised list of headers
private List<View> footers; // Lazy initialised list of footers
public void addHeader(View v) {
setDefaultLayoutParams(v);
getHeaders().add(v);
clearCache();
}
public void addFooter(View v) {
setDefaultLayoutParams(v);
getFooters().add(v);
clearCache();
}
private void setDefaultLayoutParams(View v) {
if (v.getLayoutParams() == null) {
RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT);
v.setLayoutParams(lp);
}
}
private boolean isHeaderPosition(int position) {
return position < getHeaderCount();
}
private boolean isFooterPosition(int position) {
int footerCount = getFooterCount();
if (footerCount == 0)
return false;
else
return getItemCount() - position <= footerCount;
}
private List<View> getHeaders() {
if (headers == null)
headers = new ArrayList<>();
return headers;
}
private List<View> getFooters() {
if (footers == null)
footers = new ArrayList<>();
return footers;
}
public int getHeaderCount() {
return headers == null ? 0 : headers.size();
}
public int getFooterCount() {
return footers == null ? 0 : footers.size();
}
private static class HeaderFooterHolder extends RecyclerView.ViewHolder {
public HeaderFooterHolder(View itemView) {
super(itemView);
}
}
// position conversion and caching for sections
// ==============================================================================================
private LruCache<Integer, Integer> sectionIndex;
private LruCache<Integer, Integer> sectionPosition;
LruCache<Integer, Integer> recyclerToWrappedPosition;
private LruCache<Integer, Integer> wrappedToRecyclerPosition;
private boolean lruCacheEnabled;
private int getSectionIndex(int position) {
position -= getHeaderCount(); // offset number of headers
if (lruCacheEnabled) {
return sectionIndex.get(position);
} else {
return sections.getSectionIndex(position);
}
}
private int getSectionPosition(int index) {
if (lruCacheEnabled) {
return sectionPosition.get(index);
} else {
return sections.getSectionPosition(index);
}
}
private void initPositionCaching(int cacheSize) {
lruCacheEnabled = sections.lruCacheEnabled();
if (lruCacheEnabled) {
sectionIndex = new LruCache<Integer, Integer>(cacheSize) {
@Override protected Integer create(Integer position) {
return sections.getSectionIndex(position - getHeaderCount());
}
};
sectionPosition = new LruCache<Integer, Integer>(cacheSize) {
@Override protected Integer create(Integer index) {
return sections.getSectionPosition(index);
}
};
}
recyclerToWrappedPosition = new LruCache<Integer, Integer>(cacheSize) {
@Override protected Integer create(Integer position) {
int sectionIndexVal;
int numberOfSectionsBeforePosition = 0;
// find the 1st section position before requested position
for (int i = position; i >= getHeaderCount(); i--) {
sectionIndexVal = getSectionIndex(i);
if (sectionIndexVal != NOT_A_SECTION) {
// found a section, there are that amount of sections before `position`
numberOfSectionsBeforePosition = sectionIndexVal + 1;
break;
}
}
return position - numberOfSectionsBeforePosition - getHeaderCount();
}
};
wrappedToRecyclerPosition = new LruCache<Integer, Integer>(cacheSize) {
@Override protected Integer create(Integer position) {
int value = position;
for (int i = 0; i < sections.getSectionCount(); i++) {
if (getSectionPosition(i) > value) {
break;
} else {
value++;
}
}
return value + getHeaderCount();
}
};
}
private void clearCache() {
if (sectionIndex != null) sectionIndex.evictAll();
if (sectionPosition != null) sectionPosition.evictAll();
recyclerToWrappedPosition.evictAll();
wrappedToRecyclerPosition.evictAll();
}
// Data observing
// ==============================================================================================
private final RecyclerView.AdapterDataObserver dataObserver = new RecyclerView.AdapterDataObserver() {
@Override public void onChanged() {
notifyDataSetChanged();
}
@Override public void onItemRangeChanged(int positionStart, int itemCount) {
notifyItemRangeChanged(wrappedToRecyclerPosition.get(positionStart), itemCount);
}
@Override public void onItemRangeInserted(int positionStart, int itemCount) {
// TODO: section after this point will `blink` on screen
notifyItemRangeInserted(wrappedToRecyclerPosition.get(positionStart), itemCount);
}
@Override public void onItemRangeRemoved(int positionStart, int itemCount) {
// TODO: section after this point will `blink` on screen
notifyItemRangeRemoved(wrappedToRecyclerPosition.get(positionStart), itemCount);
}
@Override public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
// TODO: moved? what if there was a header in the middle
int from = wrappedToRecyclerPosition.get(fromPosition);
int to = wrappedToRecyclerPosition.get((toPosition));
for (int i = 0; i < itemCount; i++)
notifyItemMoved(from + i, to + i);
}
};
// Goodies for GridLayoutManager
// ==============================================================================================
/**
* Generates a `GridLayoutManager.SpanSizeLookup` that makes each section
* take the whole `spanCount` and other adapter positions are 1 span each
*
* @param spanCount the number of spans
* @return a new GridLayoutManager.SpanSizeLookup to be used with this WrapAdapter
*/
public GridLayoutManager.SpanSizeLookup createSpanSizeLookup(int spanCount) {
return new SectionSpanSizeLookup(null, spanCount);
}
/**
* Generates a `GridLayoutManager.SpanSizeLookup` that makes each section
* take the whole `spanCount`, whilst keeping the wrapped SpanSizeLookup for other positions.
*
* @param wrap the SpanSizeLookup to wrap from
* @param spanCount the number of spans
* @return a new GridLayoutManager.SpanSizeLookup to be used with this WrapAdapter
*/
public GridLayoutManager.SpanSizeLookup createSpanSizeLookup(GridLayoutManager.SpanSizeLookup wrap, int spanCount) {
return new SectionSpanSizeLookup(wrap, spanCount);
}
private final class SectionSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
private final GridLayoutManager.SpanSizeLookup source;
private final int spanCount;
private SectionSpanSizeLookup(GridLayoutManager.SpanSizeLookup source, int spanCount) {
this.source = source;
this.spanCount = spanCount;
}
@Override public int getSpanSize(int position) {
if (isHeaderPosition(position)) {
return spanCount; // header take the whole width
} else if (isFooterPosition(position)) {
return spanCount; // footer take the whole width
} else {
int sectionIndexVal = getSectionIndex(position);
if (sectionIndexVal != NOT_A_SECTION) {
return spanCount; // sections take the whole width
} else if (source == null) {
return 1; // default behavior, every item is 1 span
} else {
return source.getSpanSize(recyclerToWrappedPosition.get(position));
}
}
}
}
}