Last active
July 13, 2016 14:56
-
-
Save chantellosejo/484da07a5c95de7034553803eb34c9b9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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