twotoasters / SectionCursorAdapter

Apache License 2.0
110 stars 18 forks source link

RecyclerView version #20

Closed deakjahn closed 9 years ago

deakjahn commented 9 years ago

For your pleasure and delectation, this is a version I use successfully with a RW:

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import android.content.Context;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.support.v7.widget.RecyclerView;
import android.widget.SectionIndexer;

public abstract class SectionCursorRecyclerViewAdapter<T extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<T> implements SectionIndexer {
  public static final int NO_CURSOR_POSITION = -99;
  public static final int VIEW_TYPE_NORMAL = 0;
  public static final int VIEW_TYPE_SECTION = 1;

  private Cursor currentCursor;
  private boolean isDataValid;
  private final DataSetObserver dataSetObserver;
  protected SortedMap<Integer, Object> Sections = new TreeMap<Integer, Object>();
  protected ArrayList<Integer> sectionList = new ArrayList<Integer>();
  private Object[] fastScrollObjects;

  public SectionCursorRecyclerViewAdapter(Context context, Cursor cursor) {
    currentCursor = cursor;
    isDataValid = (cursor != null);
    dataSetObserver = new NotifyingDataSetObserver();
    if (currentCursor != null)
      currentCursor.registerDataSetObserver(dataSetObserver);
  }

  public Cursor getCursor() {
    return currentCursor;
  }

  protected boolean hasOpenCursor() {
    final Cursor cursor = getCursor();
    if (cursor == null || cursor.isClosed()) {
      swapCursor(null);
      return false;
    }
    return true;
  }

  @Override
  public int getItemCount() {
    return (isDataValid && currentCursor != null) ? currentCursor.getCount() + Sections.size() : 0;
  }

  @Override
  public long getItemId(int position) {
    if (isSection(position))
      return position;
    else {
      final int cursorPosition = getCursorPositionWithoutSections(position);
      final Cursor cursor = getCursor();
      if (hasOpenCursor() && cursor.moveToPosition(cursorPosition))
        return cursor.getLong(cursor.getColumnIndex("_id"));
      else
        return NO_CURSOR_POSITION;
    }
  }

  @Override
  public int getItemViewType(int position) {
    return isSection(position) ? VIEW_TYPE_SECTION : VIEW_TYPE_NORMAL;
  }

  @Override
  public void setHasStableIds(boolean hasStableIds) {
    super.setHasStableIds(true);
  }

  public abstract void onBindViewHolder(T viewHolder, Cursor cursor);

  @Override
  public void onBindViewHolder(T viewHolder, int position) {
    if (!isDataValid)
      throw new IllegalStateException("invalid cursor");
    if (!currentCursor.moveToPosition(position))
      throw new IllegalStateException("couldn't move cursor to position " + position);
    onBindViewHolder(viewHolder, currentCursor);
  }

  public void changeCursor(Cursor cursor) {
    final Cursor old = swapCursor(cursor);
    if (old != null)
      old.close();
  }

  public Cursor swapCursor(Cursor newCursor) {
    if (newCursor == currentCursor)
      return null;
    final Cursor oldCursor = currentCursor;
    if (oldCursor != null && dataSetObserver != null)
      oldCursor.unregisterDataSetObserver(dataSetObserver);
    currentCursor = newCursor;
    if (currentCursor != null) {
      if (dataSetObserver != null)
        currentCursor.registerDataSetObserver(dataSetObserver);
      isDataValid = true;
      buildSections(currentCursor);
      notifyDataSetChanged();
    }
    else {
      isDataValid = false;
      notifyDataSetChanged();
    }
    return oldCursor;
  }

  protected abstract Object getSectionFromCursor(Cursor cursor);

  protected void buildSections() {
    if (hasOpenCursor()) {
      final Cursor cursor = getCursor();
      cursor.moveToPosition(-1);
      buildSections(cursor);
      if (Sections == null)
        Sections = new TreeMap<Integer, Object>();
    }
  }

  protected void buildSections(Cursor cursor) {
    Sections.clear();
    int cursorPosition = 0;
    while (hasOpenCursor() && cursor.moveToNext()) {
      final Object sectionObject = getSectionFromCursor(cursor);
      if (cursor.getPosition() != cursorPosition)
        throw new IllegalStateException("cursor moved");
      if (!Sections.containsValue(sectionObject))
        Sections.put(cursorPosition + Sections.size(), sectionObject);
      cursorPosition++;
    }
  }

  public boolean isSection(int position) {
    return Sections.containsKey(position);
  }

  public int getCursorPositionWithoutSections(int position) {
    if (Sections.size() == 0)
      return position;
    else if (!isSection(position)) {
      final int sectionIndex = getIndexWithinSections(position);
      if (isListPositionBeforeFirstSection(position, sectionIndex))
        return position;
      else
        return position - (sectionIndex + 1);
    }
    else
      return NO_CURSOR_POSITION;
  }

  public int getIndexWithinSections(int position) {
    boolean isSection = false;
    int numPrecedingSections = 0;
    for (final Integer sectionPosition : Sections.keySet())
      if (position > sectionPosition)
        numPrecedingSections++;
      else if (position == sectionPosition)
        isSection = true;
      else
        break;
    return isSection ? numPrecedingSections : Math.max(numPrecedingSections - 1, 0);
  }

  private boolean isListPositionBeforeFirstSection(int position, int sectionIndex) {
    final boolean hasSections = (Sections != null && Sections.size() > 0);
    return (sectionIndex == 0 && hasSections && position < Sections.firstKey());
  }

  public Object getSection(int position) {
    return Sections.get(position);
  }

  @Override
  public int getPositionForSection(int sectionIndex) {
    if (sectionList.size() == 0)
      for (final Integer key : Sections.keySet())
        sectionList.add(key);
    return (sectionIndex < sectionList.size()) ? sectionList.get(sectionIndex) : getItemCount();
  }

  @Override
  public int getSectionForPosition(int position) {
    final Object[] objects = getSections();
    final int sectionIndex = getIndexWithinSections(position);
    return sectionIndex < objects.length ? sectionIndex : 0;
  }

  @Override
  public Object[] getSections() {
    if (fastScrollObjects == null)
      fastScrollObjects = getFastScrollDialogLabels();
    return fastScrollObjects;
  }

  protected int getMaxIndexerLength() {
    return 3;
  }

  private Object[] getFastScrollDialogLabels() {
    final Collection<Object> sectionsCollection = Sections.values();
    final Object[] objects = sectionsCollection.toArray(new Object[sectionsCollection.size()]);
    if (VERSION.SDK_INT < VERSION_CODES.KITKAT) {
      final int max = getMaxIndexerLength();
      for (int i = 0; i < objects.length; i++)
        if (objects[i].toString().length() >= max)
          objects[i] = objects[i].toString().substring(0, max);
    }
    return objects;
  }

  private class NotifyingDataSetObserver extends DataSetObserver {
    @Override
    public void onChanged() {
      super.onChanged();
      isDataValid = true;
      buildSections();
      notifyDataSetChanged();
    }

    @Override
    public void onInvalidated() {
      super.onInvalidated();
      isDataValid = false;
      buildSections();
      notifyDataSetChanged();
    }
  }
}

Sorry for the variable name changes, no matter how conventional it is, I just can't force myself to use mNames... :-)

deakjahn commented 9 years ago

Usage, although it's quite obvious, in the adapter extended from the above:

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  if (viewType == VIEW_TYPE_SECTION) {
    View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.listheader, parent, false);
    return new HeaderViewHolder(itemView);
  }
  else {
    View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.listitem, parent, false);
    return new ItemViewHolder(itemView);
  }
}

and

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, Cursor cursor) {
  if (holder instanceof HeaderViewHolder) {
    HeaderViewHolder view = (HeaderViewHolder) holder;
    ...
  }
  else {
    ItemViewHolder view = (ItemViewHolder) holder;
    ...
  }
}
MinceMan commented 9 years ago

Internally we don't name with m ourselves. I do it for open source to be compatible with Android guidelines.

Other comments: The changeCursor method concerns me for two reasons; 1. it bypasses the swapCursor method which all of the data is build on, I see a lot of index and null errors occuring from this, and 2. you should never close a cursor in an adapter because that adapter does not own that cursor.

I would like to see a getCursor method for general usage.

I would also like to see a createSectionViewHolder(), bindSectionViewHolder(), createItemViewHolder() and bindItemViewHolder(). It would remove that simple boiler plate logic out of the implementing class.

Looks good. I would take this as a pull request with the above changes.

deakjahn commented 9 years ago

Yes, you caught me. I actually use this class implementing a CursorLoader itself, so it does own the cursor all right (and if it doesn't close them, strict mode results in an exception, of course). When I edited the loader out for more general use, those closes might have remained by mistake.

As to the rest, I dunno. You did it that way in the original version, yes, but the usual RW way seems to be this one, sending the view type up to the using class. In my actual case, this happens to be better because I reuse this same class over and over again to display different sectioned lists...

MinceMan commented 9 years ago

SectionCursorAdapter 3.0 now has experimental RecyclerViewAdapters.