Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@chanakin
Last active July 13, 2016 14:56
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chanakin/484da07a5c95de7034553803eb34c9b9 to your computer and use it in GitHub Desktop.
Save chanakin/484da07a5c95de7034553803eb34c9b9 to your computer and use it in GitHub Desktop.
package com.yourpackage.android;
import android.content.Context;
import android.os.Parcelable;
import android.support.annotation.IdRes;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.yourpackage.android.R;
import java.util.ArrayList;
/**
* Base adapter class for handling sectioned lists, such as grocery list, menu planner, and our browse experience.
*/
public class SectionedRecyclerViewAdapter<T extends Parcelable> extends BaseRecyclerViewAdapter<RecyclerView.ViewHolder> {
private boolean valid = true;
private final int sectionResourceId;
private final int textResourceId;
private final int dividerResourceId;
private final RecyclerView.Adapter baseAdapter;
final SparseArray<Section<T>> sections = new SparseArray<>();
private ArrayList<Section<T>> unpositionedSections;
public SectionedRecyclerViewAdapter(Context context, SectionedAdapterParameters parameters) {
super(context, parameters.headerViewType, parameters.footerViewType);
this.sectionResourceId = parameters.sectionResourceId;
this.textResourceId = parameters.textResourceId;
this.dividerResourceId = parameters.dividerResourceId;
this.baseAdapter = parameters.baseAdapter;
this.inflater = LayoutInflater.from(context);
this.baseAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
valid = SectionedRecyclerViewAdapter.this.baseAdapter.getItemCount() > 0;
notifyDataSetChanged();
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
valid = SectionedRecyclerViewAdapter.this.baseAdapter.getItemCount() > 0;
notifyItemRangeChanged(convertUnderlyingListDataPositionToAdapterPosition(positionStart), itemCount);
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
valid = SectionedRecyclerViewAdapter.this.baseAdapter.getItemCount() > 0;
notifyItemRangeInserted(convertUnderlyingListDataPositionToAdapterPosition(positionStart), itemCount);
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
valid = SectionedRecyclerViewAdapter.this.baseAdapter.getItemCount() > 0;
notifyItemRangeRemoved(convertUnderlyingListDataPositionToAdapterPosition(positionStart), itemCount);
// TODO come back to this. I'm not sure if the proper thing here to do is to modify the sections or
// if it should be expected of the caller to have setSections() with the proper data.
/**
int childItemsCount = baseAdapter.getItemCount();
if (childItemsCount == 0) {
unpositionedSections.clear();
sections.clear();
notifyDataSetChanged();
return;
}
ArrayList<ListSection> remainingSections = new ArrayList<>();
SparseArray<ListSection> remainingPositionedSections = new SparseArray<>();
int convertedSectionedPositionStart = convertUnderlyingListDataPositionToSectionedPosition(positionStart);
int indexOfLastItemRemoved = convertedSectionedPositionStart + itemCount - 1;
int removedSectionCount = 0;
for (int i = 0; i < unpositionedSections.size(); i++) {
ListSection section = unpositionedSections.get(i);
if (removeEmptySections) {
// First, check and see if an entire section has been removed. Don't worry, this will not be affected by any previous section removals.
if (i < unpositionedSections.size() - 1) {
if (convertedSectionedPositionStart < section.sectionedPosition || convertedSectionedPositionStart == section.sectionedPosition + 1) {
ListSection nextSection = unpositionedSections.get(i + 1);
int numItemsInSection = nextSection.firstPosition - section.firstPosition;
if (indexOfLastItemRemoved >= section.sectionedPosition + numItemsInSection) {
++removedSectionCount;
continue;
}
}
} else if (positionStart == childItemsCount && section.firstPosition == positionStart) {
++removedSectionCount;
continue;
}
}
if (section.sectionedPosition > convertedSectionedPositionStart) {
if (indexOfLastItemRemoved < section.sectionedPosition) {
section.firstPosition -= itemCount;
} else {
//TODO this is probably not as efficient as it could be, but my brain has turned to mush.
int numItemsAfterSection = indexOfLastItemRemoved - section.sectionedPosition;
int numItemsBeforeSection = itemCount - numItemsAfterSection;
section.firstPosition = section.firstPosition - numItemsBeforeSection;
}
}
remainingSections.add(section);
section.sectionedPosition = section.firstPosition + i - removedSectionCount;
remainingPositionedSections.append(section.sectionedPosition, section);
}
unpositionedSections = remainingSections;
sections = remainingPositionedSections;
notifyDataSetChanged();
**/
}
});
}
public static class SectionViewHolder extends RecyclerView.ViewHolder {
public final TextView title;
public final View divider;
public SectionViewHolder(View view, int textResourceId, int dividerResourceId) {
super(view);
title = (TextView) view.findViewById(textResourceId);
divider = view.findViewById(dividerResourceId);
}
}
@Override
public final RecyclerView.ViewHolder onCreateListItemViewHolder(ViewGroup parent, int typeView) {
if (typeView == getSectionListItemViewType()) {
return onCreateSectionViewHolder(parent, typeView);
}
return baseAdapter.onCreateViewHolder(parent, typeView);
}
@Override
public final void onBindListItemViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
if (isSectionHeaderPosition(position)) {
onBindSectionViewHolder(viewHolder, position);
return;
}
//noinspection unchecked
baseAdapter.onBindViewHolder(viewHolder, convertSectionedPositionToUnderlyingDataPosition(position));
}
// Subclasses can override these methods to build out a custom view for the section
// This lets us subclass and reuse all this lovely code for ads (which we'll consider a "section header" for our purposes)
RecyclerView.ViewHolder onCreateSectionViewHolder(ViewGroup parent, int typeView) {
final View view = inflater.inflate(sectionResourceId, parent, false);
return new SectionViewHolder(view, textResourceId, dividerResourceId);
}
void onBindSectionViewHolder(final RecyclerView.ViewHolder viewHolder, final int position) {
((SectionViewHolder) viewHolder).title.setText(sections.get(position).getTitle());
((SectionViewHolder) viewHolder).divider.setVisibility(position == 0 ? View.GONE : View.VISIBLE);
if (position - getHeaderCount() <= 0) {
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) ((SectionViewHolder) viewHolder).title.getLayoutParams();
layoutParams.setMargins(layoutParams.leftMargin, 0, layoutParams.rightMargin, layoutParams.bottomMargin);
((SectionViewHolder) viewHolder).title.setLayoutParams(layoutParams);
}
}
@Override
public int getListItemViewType(int position) {
if (isSectionHeaderPosition(position)) {
return getSectionListItemViewType();
}
return baseAdapter.getItemViewType(convertSectionedPositionToUnderlyingDataPosition(position));
}
@IdRes
int getSectionListItemViewType() {
return R.id.section_header_list_item;
}
/**
* Sets the sections. This method will handle positioning them appropriately given
* that you properly return the initial size of the section.
* <p/>
* Please note that these sections must be sorted prior to being set here, as this
* method will not handle sorting any items on the behalf of the user. The positions
* will be set assuming that the list is in order.
*
* @param sections The list sections to be displayed
*/
//noinspection unchecked
public void setSections(@NonNull ArrayList<? extends Section<T>> sections) {
valid = false;
if (unpositionedSections != null) {
unpositionedSections.clear();
}
addSections(sections, 0, false);
}
/**
* Adds a single section at the index specified.
*
* @param sectionToAdd section to be added to this adapter
* @param index index where the section should be added
*/
public void addSection(@NonNull Section sectionToAdd, @IntRange(from = 0) int index) {
valid = false;
if (unpositionedSections == null) {
unpositionedSections = new ArrayList<>();
}
if (index >= unpositionedSections.size()) {
//noinspection unchecked
unpositionedSections.add(sectionToAdd);
} else {
//noinspection unchecked
unpositionedSections.add(index, sectionToAdd);
}
setSectionPositions();
final int adapterPosition = convertUnderlyingSectionedDataPositionToAdapterPosition(sectionToAdd.sectionedPosition);
// Don't forget to include the section itself in that item count...only took me two hours to realize this was the issue causing everything to duplicate randomly.
notifyItemRangeInserted(adapterPosition, sectionToAdd.getSize() + 1);
}
public void removeSection(@IntRange(from = 0) int index) {
valid = false;
if (unpositionedSections == null) {
unpositionedSections = new ArrayList<>();
}
if (index >= unpositionedSections.size()) {
return;
}
Section sectionToRemove = unpositionedSections.get(index);
unpositionedSections.remove(index);
setSectionPositions();
final int adapterPosition = convertUnderlyingSectionedDataPositionToAdapterPosition(sectionToRemove.sectionedPosition);
// Don't forget to include the section itself in that item count...only took me two hours to realize this was the issue causing everything to duplicate randomly.
notifyItemRangeRemoved(adapterPosition, sectionToRemove.getSize() + 1);
}
private void addSections(@NonNull ArrayList<? extends Section<T>> sectionsToAdd, @IntRange(from = 0) int startIndex, boolean notifyDataSetChanged) {
if (sectionsToAdd.isEmpty()) {
if (notifyDataSetChanged) {
notifyDataSetChanged();
}
return;
}
valid = false;
if (unpositionedSections == null) {
unpositionedSections = new ArrayList<>();
}
if (startIndex > unpositionedSections.size()) {
unpositionedSections.addAll(sectionsToAdd);
} else {
unpositionedSections.addAll(startIndex, sectionsToAdd);
}
setSectionPositions();
if (notifyDataSetChanged) {
Section firstSectionAdded = sectionsToAdd.get(0);
final int startAdapterPosition = convertUnderlyingSectionedDataPositionToAdapterPosition(firstSectionAdded.sectionedPosition);
int addedItemCount = sectionsToAdd.size();
for (Section section : sectionsToAdd) {
addedItemCount += section.getSize();
}
notifyItemRangeInserted(startAdapterPosition, addedItemCount);
}
}
public void addSections(@NonNull ArrayList<? extends Section<T>> sectionsToAdd, @IntRange(from = 0) int startIndex) {
addSections(sectionsToAdd, startIndex, true);
}
private void setSectionPositions() {
int endOfLastSectionPosition = 0;
int sectionPosOffset = 0;
this.sections.clear();
for (int i = 0; i < unpositionedSections.size(); i++) {
Section section = unpositionedSections.get(i);
if (section.getSize() == 0 && section.contentRequiredForDisplay()) {
continue;
}
section.firstPosition = endOfLastSectionPosition;
section.sectionedPosition = section.firstPosition + sectionPosOffset;
//noinspection unchecked
this.sections.append(section.sectionedPosition, section);
endOfLastSectionPosition += section.getSize();
++sectionPosOffset;
}
valid = true;
}
public int convertUnderlyingSectionedDataPositionToAdapterPosition(int position) {
return super.convertUnderlyingListDataPositionToAdapterPosition(position);
}
@Override
public int convertUnderlyingListDataPositionToAdapterPosition(int position) {
return super.convertUnderlyingListDataPositionToAdapterPosition(convertUnderlyingListDataPositionToSectionedPosition(position));
}
private int convertUnderlyingListDataPositionToSectionedPosition(int position) {
int offset = 0;
for (int i = 0; i < sections.size(); i++) {
if (sections.valueAt(i).firstPosition > position) {
break;
}
++offset;
}
return position + offset;
}
private int convertSectionedPositionToUnderlyingDataPosition(int position) {
int offset = 0;
for (int i = 0; i < sections.size(); i++) {
if (sections.valueAt(i).sectionedPosition > position) {
break;
}
++offset;
}
return position - offset;
}
private boolean isSectionHeaderPosition(int position) {
return sections.get(position) != null;
}
@Override
public long getListItemId(int position) {
if (isSectionHeaderPosition(position)) {
return Integer.MAX_VALUE - sections.indexOfKey(position);
}
return baseAdapter.getItemId(convertSectionedPositionToUnderlyingDataPosition(position));
}
@Override
public int getListItemCount() {
return valid ? baseAdapter.getItemCount() + sections.size() : 0;
}
public void addHeaderView(View headerView) {
super.addHeaderView(headerView);
updateAfterHeaderFooterChange();
}
public void removeHeaderView(View headerView) {
super.removeHeaderView(headerView);
updateAfterHeaderFooterChange();
}
public void addFooterView(View footerView) {
super.addFooterView(footerView);
updateAfterHeaderFooterChange();
}
public void removeFooterView(View footerView) {
super.removeFooterView(footerView);
updateAfterHeaderFooterChange();
}
/**
* This is a bit of a hack to "ensure" that
* we don't have out of bounds exceptions if the
* caller decides to remove/add header/footers on the fly...
* if that happens, then we must recalculate the sections' positions
*/
private void updateAfterHeaderFooterChange() {
if (unpositionedSections != null) {
setSections(unpositionedSections);
}
notifyDataSetChanged();
}
public RecyclerView.Adapter getBaseAdapter() {
return baseAdapter;
}
public int getPositionOfSection(int indexOfSection) {
if (indexOfSection < unpositionedSections.size()) {
return unpositionedSections.get(indexOfSection).sectionedPosition;
}
return -1;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment