Skip to content

Instantly share code, notes, and snippets.

@TonicArtos
Created November 17, 2015 23:27
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 TonicArtos/c5df9906547655e3de94 to your computer and use it in GitHub Desktop.
Save TonicArtos/c5df9906547655e3de94 to your computer and use it in GitHub Desktop.
SuperSLiM Adapter - Not quite finished.
package com.tonicartos.superslim.adapter;
/**
*
*/
public interface Item {
int getType();
}
package com.tonicartos.superslim.adapter;
import com.tonicartos.superslim.BuildConfig;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
class ItemManager {
private final ArrayList<ItemNode> mItems;
private SectionGraphAdapter mSectionGraphAdapter;
private boolean mInMassRemoval;
public ItemManager(SectionGraphAdapter sectionGraphAdapter) {
mSectionGraphAdapter = sectionGraphAdapter;
mItems = new ArrayList<>();
}
public ItemNode get(int position) {
return mItems.get(position);
}
public void insert(int position, ItemNode item) {
mItems.add(position, item);
if (BuildConfig.DEBUG) {
// TODO: Change this check to BuildConfig.FLAVOUR == "test".
return;
}
mSectionGraphAdapter.notifyItemInserted(position);
}
public void insert(int start, List<ItemNode> items) {
for (int i = 0; i < items.size(); i++) {
mItems.add(start + i, items.get(i));
}
if (BuildConfig.DEBUG) {
return;
}
mSectionGraphAdapter.notifyItemRangeInserted(start, items.size());
}
public void move(int from, int to) {
to -= (from < to) ? 1 : 0;
mItems.add(to, mItems.remove(from));
if (BuildConfig.DEBUG) {
return;
}
mSectionGraphAdapter.notifyItemMoved(from, to);
}
public void remove(int position) {
mItems.remove(position);
if (BuildConfig.DEBUG) {
return;
}
mSectionGraphAdapter.notifyItemRemoved(position);
}
public void removeRange(int start, int range) {
for (int i = 0; i < range; i++) {
mItems.remove(start);
}
if (BuildConfig.DEBUG) {
return;
}
mSectionGraphAdapter.notifyItemRangeRemoved(start, range);
}
public void update(int position, ItemNode item) {
mItems.set(position, item);
if (BuildConfig.DEBUG) {
return;
}
mSectionGraphAdapter.notifyItemChanged(position);
}
int getItemCount() {
return mItems.size();
}
}
package com.tonicartos.superslim.adapter;
/**
*
*/
class ItemNode extends Node {
private final Item mItem;
ItemNode(Section parent, Item data) {
mItem = data;
setParent(parent);
}
@Override
public int getChildCount() {
return 0;
}
@Override
public int getTotalItems() {
return 1;
}
@Override
public boolean isItem() {
return true;
}
@Override
public boolean isSection() {
return false;
}
@Override
void insertItemsToAdapter() {
mItemManager.insert(getPositionInAdapter(), this);
}
@Override
void removeItemsFromAdapter() {
mItemManager.remove(getPositionInAdapter());
}
@Override
public String toString() {
return mItem.toString();
}
Item getItem() {
return mItem;
}
}
package com.tonicartos.superslim.adapter;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.List;
/**
*
*/
abstract class Node {
protected static final ItemManager DUMMY_MANAGER = new DummyItemManager();
@NonNull
protected ItemManager mItemManager = DUMMY_MANAGER;
@Nullable
protected Section mParent;
protected int mPositionInParent;
private int mPeersItemsBeforeThis;
public abstract int getChildCount();
@Nullable
public Section getParent() {
return mParent;
}
void setParent(@NonNull Section parent) {
mParent = parent;
}
public int getPositionInAdapter() {
if (mParent == null) {
return 0;
}
return mParent.getPositionInAdapter() + mPeersItemsBeforeThis;
}
public int getPositionInParent() {
return mPositionInParent;
}
public void setPositionInParent(int positionInParent) {
mPositionInParent = positionInParent;
}
public abstract int getTotalItems();
public abstract boolean isItem();
public abstract boolean isSection();
void changePeersItemsBeforeThis(int change) {
mPeersItemsBeforeThis += change;
}
void changePositionInParent(int change) {
mPositionInParent += change;
}
int getPeersItemsBeforeThis() {
return mPeersItemsBeforeThis;
}
void setPeersItemsBeforeThis(int itemsBefore) {
mPeersItemsBeforeThis = itemsBefore;
}
abstract void insertItemsToAdapter();
abstract void removeItemsFromAdapter();
void reset() {
mParent = null;
setItemManager(DUMMY_MANAGER);
}
void setItemManager(@NonNull ItemManager itemManager) {
mItemManager = itemManager;
}
public static class DummyItemManager extends ItemManager {
public DummyItemManager() {
super(null);
}
@Override
public ItemNode get(int position) {
return null;
}
@Override
public void insert(int position, ItemNode item) {
}
@Override
public void insert(int start, List<ItemNode> items) {
}
@Override
public void move(int from, int to) {
}
@Override
public void remove(int position) {
}
@Override
public void removeRange(int start, int range) {
}
@Override
public void update(int position, ItemNode item) {
}
@Override
int getItemCount() {
return 0;
}
}
}
package com.tonicartos.superslim.adapter;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
/**
*
*/
public class Section extends Node {
private ItemNode mHeader;
private ArrayList<Node> mChildren = new ArrayList<>();
private int mTotalItems;
@Nullable
private SectionConfiguration mConfiguration;
private SectionGraphAdapter.Registration mRegistration;
private boolean mIsCollapsed;
Section() {
}
Section(SectionGraphAdapter.Registration registration) {
mRegistration = registration;
}
public final void add(Section section) {
insert(mChildren.size(), section);
}
public final void add(@NonNull Item item) {
insert(mChildren.size(), item);
}
public void collapseChildren() {
int jumpHeader = mHeader == null ? 0 : 1;
int numItemsToRemove = getTotalItems() - jumpHeader;
mItemManager.removeRange(getPositionInAdapter() + jumpHeader, numItemsToRemove);
for (int i = 0, size = mChildren.size(); i < size; i++) {
mChildren.get(i).reset();
}
mTotalItems -= numItemsToRemove;
if (mParent != null) {
mParent.totalItemsChanged(mPositionInParent, -numItemsToRemove);
}
mIsCollapsed = true;
}
/**
* Deregisters this section and all descendant sections from the adapter.
*/
public void deregister() {
for (int i = 0, size = mChildren.size(); i < size; i++) {
if (mChildren.get(i).isSection()) {
((Section) mChildren.get(i)).deregister();
}
}
mRegistration.deregister();
}
public void expandChildren() {
int numItemsAdded = 0;
for (int i = 0, size = mChildren.size(); i < size; i++) {
Node child = mChildren.get(i);
initChild(i, child);
// Add actual item hierarchy to adapter and this section.
child.insertItemsToAdapter();
// Update the number of descendants for this section.
numItemsAdded += child.getTotalItems();
}
totalItemsChanged(numItemsAdded);
mIsCollapsed = false;
}
/**
* Warning! This method has an unchecked generic return. Return type will be one of Item or
* Section. To be safe you should check the item type first by calling {@link
* Section#isPositionItem(int)} or {@link Section#isPositionSection(int)}.
*/
public <T> T get(int position) {
Node node = mChildren.get(position);
if (node.isItem()) {
return (T) ((ItemNode) node).getItem();
}
return (T) node;
}
public int getAdapterPositionOfChild(int position) {
return mChildren.get(position).getPositionInAdapter();
}
@Override
public int getChildCount() {
return mChildren.size();
}
@Override
public int getTotalItems() {
return mTotalItems;
}
@Override
public boolean isItem() {
return false;
}
@Override
public boolean isSection() {
return true;
}
@Override
void insertItemsToAdapter() {
if (mHeader != null) {
mHeader.setItemManager(mItemManager);
mItemManager.insert(getPositionInAdapter(), mHeader);
}
if (!mIsCollapsed) {
for (int i = 0, size = mChildren.size(); i < size; i++) {
Node child = mChildren.get(i);
child.setItemManager(mItemManager);
child.insertItemsToAdapter();
}
}
}
@Override
void removeItemsFromAdapter() {
mItemManager.removeRange(getPositionInAdapter(), getTotalItems());
}
@Override
void setItemManager(@NonNull ItemManager itemManager) {
super.setItemManager(itemManager);
for (int i = 0, size = mChildren.size(); i < size; i++) {
mChildren.get(i).setItemManager(itemManager);
}
}
public ItemNode getHeader() {
return mHeader;
}
public void setHeader(@NonNull Item item) {
ItemNode header = new ItemNode(this, item);
header.setItemManager(mItemManager);
header.setPeersItemsBeforeThis(0);
if (mHeader != null) {
mItemManager.update(getPositionInAdapter(), header);
} else {
// Now have header.
mItemManager.insert(getPositionInAdapter(), header);
// Update children as we have inserted an item before them.
for (int i = 0, size = mChildren.size(); i < size; i++) {
Node child = mChildren.get(i);
child.changePeersItemsBeforeThis(1);
}
totalItemsChanged(1);
}
mHeader = header;
}
public final void insert(int position, Section section) {
insertChild(position, section);
}
public final void insert(int position, @NonNull Item item) {
insertChild(position, new ItemNode(this, item));
}
public boolean isCollapsed() {
return mIsCollapsed;
}
public boolean isPositionItem(int position) {
return mChildren.get(position).isItem();
}
public boolean isPositionSection(int position) {
return mChildren.get(position).isSection();
}
public final void remove(int position) {
final Node removed = mChildren.remove(position);
if (!mIsCollapsed) {
removed.removeItemsFromAdapter();
final int itemChange = -removed.getTotalItems();
// Update every child after the removed one.
for (int i = position + 1, size = mChildren.size(); i < size; i++) {
mChildren.get(i).changePeersItemsBeforeThis(itemChange);
mChildren.get(i).changePositionInParent(-1);
}
totalItemsChanged(itemChange);
}
removed.reset();
}
public void removeFromParent() {
if (mParent != null) {
mParent.remove(mPositionInParent);
}
}
public void removeHeader() {
if (mHeader != null) {
mHeader = null;
mItemManager.remove(getPositionInAdapter());
mTotalItems -= 1;
for (int i = 0, size = mChildren.size(); i < size; i++) {
Node child = mChildren.get(i);
child.setPeersItemsBeforeThis(child.getPeersItemsBeforeThis() - 1);
}
if (mParent != null) {
mParent.totalItemsChanged(mPositionInParent, -1);
}
}
}
public void setConfiguration(@NonNull SectionConfiguration configuration) {
mConfiguration = configuration;
}
public void toggleChildren() {
if (mIsCollapsed) {
expandChildren();
} else {
collapseChildren();
}
}
/**
* Update or replace an item.
*
* @param position Position of an item in section to perform update on. The update will fail if
* the position is that of a section.
* @param item Item to replace into the position.
* @return False if the update failed. This should only happen if the target position is a
* section and not an item.
*/
public final boolean update(int position, @NonNull Item item) {
// TODO: Test behaviour when view type changes. Might have to do a replace rather than
// update if old.getItem().getType() != item.getType().
if (mChildren.get(position).isItem()) {
ItemNode old = (ItemNode) mChildren.get(position);
ItemNode update = new ItemNode(this, item);
update.setParent(this);
update.setItemManager(mItemManager);
update.setPositionInParent(position);
update.setPeersItemsBeforeThis(old.getPeersItemsBeforeThis());
if (!mIsCollapsed) {
mItemManager.update(old.getPositionInAdapter(), update);
}
mChildren.set(position, update);
return true;
}
return false;
}
void totalItemsChanged(int childPosition, int change) {
// Update children after child that called in change.
for (int i = childPosition + 1, size = mChildren.size(); i < size; i++) {
mChildren.get(i).changePeersItemsBeforeThis(change);
}
totalItemsChanged(change);
}
private void initChild(int position, Node child) {
child.setItemManager(mItemManager);
child.setParent(this);
// Set child start position.
if (position > 0) {
Node prior = mChildren.get(position - 1);
child.setPeersItemsBeforeThis(
prior.getPeersItemsBeforeThis() + prior.getTotalItems());
} else {
child.setPeersItemsBeforeThis(mHeader == null ? 0 : 1);
}
// Set child position on child, this has to be tracked with inserts and removals but we
// already have to do that to keep the correct index into the adapter item array.
child.setPositionInParent(position);
}
private void insertChild(int position, Node child) {
if (position < 0) {
position = mChildren.size();
}
if (!mIsCollapsed) {
initChild(position, child);
// Add actual item hierarchy to adapter and this section.
child.insertItemsToAdapter();
// Update children after the inserted child.
final int numItemsAdded = child.getTotalItems();
for (int i = position, size = mChildren.size(); i < size; i++) {
mChildren.get(i).changePeersItemsBeforeThis(numItemsAdded);
mChildren.get(i).changePositionInParent(1);
}
totalItemsChanged(numItemsAdded);
}
mChildren.add(position, child);
}
private void totalItemsChanged(int numItemsAdded) {
mTotalItems += numItemsAdded;
if (mParent != null) {
mParent.totalItemsChanged(mPositionInParent, numItemsAdded);
}
}
}
package com.tonicartos.superslim.adapter;
import android.support.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
*
*/
public class SectionConfiguration {
/**
* Header is positioned at the top of the section content. Content starts below the
* header. Inline headers are always sticky. Use the embedded style if you want an
* inline header that is not sticky.
*/
public static final int HEADER_INLINE = 1;
/**
* Header is positioned at the top of the section content. Content starts below the
* header, but the header never becomes sticky. Embedded headers may not be overlays
* either.
*/
public static final int HEADER_EMBEDDED = 2;
/**
* Header is aligned to the start edge of the section. This is the left for LTR
* locales.
* <p/>
* Start aligned headers are always sticky.
*/
public static final int HEADER_START = 4;
/**
* Header is aligned to the end edge of the section. This is the right for LTR locales.
* <p/>
* End aligned headers are always sticky.
*/
public static final int HEADER_END = 8;
/**
* Overlay headers float above the content.
* <p/>
* Overlay headers are always sticky.
*/
public static final int HEADER_OVERLAY = 16;
public static final int MARGIN_AUTO = -1;
static final int CUSTOM_SLM = 0;
private static final int DEFAULT_MARGIN = MARGIN_AUTO;
private static final int DEFAULT_HEADER_STYLE = HEADER_INLINE;
private int mHeaderStyle;
private String mLabel;
private int mHeaderMarginStart;
private int mHeaderMarginEnd;
/**
* Create a new configuration for a custom slm.
*
* @param label A label assigned to a slm when it was added to the layout manager.
*/
public SectionConfiguration(String label) {
this(label, DEFAULT_MARGIN, DEFAULT_MARGIN, DEFAULT_HEADER_STYLE);
}
/**
* Create a new configuration for a custom slm. The label identifies a custom slm added to
* the layout manager.
*
* @param label A label assigned to a slm when it was added to the layout
* manager.
* @param headerMarginStart A margin for the section in which the header may be placed if
* it is start aligned. Which edge is the start edge is determined
* by the language locale.
* @param headerMarginEnd A margin for the section in which the header may be placed if
* it is end aligned. Which edge is the end edge is determined by
* the language locale.
* @param headerStyle Header style for this section.
*/
public SectionConfiguration(String label, int headerMarginStart, int headerMarginEnd,
@HeaderStyle int headerStyle) {
this(headerMarginStart, headerMarginEnd, headerStyle);
mLabel = label;
}
SectionConfiguration() {
this(DEFAULT_MARGIN, DEFAULT_MARGIN, DEFAULT_HEADER_STYLE);
}
/**
* @param headerMarginStart A margin for the section in which the header may be placed if
* it is start aligned. Which edge is the start edge is determined
* by the language locale.
* @param headerMarginEnd A margin for the section in which the header may be placed if
* it is end aligned. Which edge is the end edge is determined by
* the language locale.
* @param headerStyle Header style for this section.
*/
SectionConfiguration(int headerMarginStart, int headerMarginEnd, @HeaderStyle int headerStyle) {
mHeaderMarginStart = headerMarginStart;
mHeaderMarginEnd = headerMarginEnd;
mHeaderStyle = headerStyle;
}
public int getHeaderMarginEnd() {
return mHeaderMarginEnd;
}
public void setHeaderMarginEnd(int headerMarginEnd) {
mHeaderMarginEnd = headerMarginEnd;
}
public int getHeaderMarginStart() {
return mHeaderMarginStart;
}
public void setHeaderMarginStart(int headerMarginStart) {
mHeaderMarginStart = headerMarginStart;
}
public int getHeaderStyle() {
return mHeaderStyle;
}
public void setHeaderStyle(int headerStyle) {
mHeaderStyle = headerStyle;
}
/**
* When custom slms are added to the layout manager they are assigned a label by the
* client. The value returned by this method is expected to match one of those labels.
*
* @return A slm label that may match a known one in the layout manager.
*/
String getCustomSlmLabel() {
return mLabel;
}
/**
* Overridden for internal subclasses which have a known slm kind.
*
* @return Kind of slm.
*/
int getSlmKind() {
return CUSTOM_SLM;
}
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {HEADER_INLINE, HEADER_EMBEDDED, HEADER_START, HEADER_END,
HEADER_OVERLAY})
public @interface HeaderStyle {
}
}
package com.tonicartos.superslim.adapter;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import java.util.HashMap;
public abstract class SectionGraphAdapter<VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
private final SectionGraphRoot mSectionGraph;
private final ItemManager mItemManager = new ItemManager(this);
private HashMap<String, Section> mSectionLookup;
public SectionGraphAdapter() {
mSectionGraph = new SectionGraphRoot(mItemManager);
init();
}
public void addSection(Section section) {
mSectionGraph.add(section);
}
public Section createSection(String id) {
return createSection(id, null, null);
}
public Section createSection(String id, @Nullable SectionConfiguration configuration,
@Nullable Item header) {
Section section = new Section(new Registration(id, mSectionLookup));
if (header != null) {
section.setHeader(header);
}
if (configuration != null) {
section.setConfiguration(configuration);
}
registerSection(id, section);
return section;
}
/**
* Deregisters the section from the adapter. This removes the reference that allows the section
* to be accessed directly without having to travers the section graph. Deregistration cascades
* to descended sections. To detach the section from the graph you can call {@link
* Section#removeFromParent()}.
*
* @param id Id by which the parent section of the subgraph is referred by.
* @return Root section of the subgraph.
*/
public Section deregisterSection(String id) {
Section section = mSectionLookup.get(id);
section.deregister();
return section;
}
public int getNumSections() {
return mSectionGraph.getChildCount();
}
public Section getSection(String id) {
return mSectionLookup.get(id);
}
public Section getSection(int position) {
return (Section) mSectionGraph.get(position);
}
public void insertSection(int position, Section section) {
mSectionGraph.insert(position, section);
}
@Override
public final void onBindViewHolder(VH holder, int position) {
ItemNode itemNode = mItemManager.get(position);
onBindViewHolder(holder, itemNode.getItem());
}
@Override
public int getItemViewType(int position) {
return mItemManager.get(position).getItem().getType();
}
@Override
public int getItemCount() {
return mItemManager.getItemCount();
}
public abstract void onBindViewHolder(VH holder, Item item);
/**
* Remove a section from the graph. This section will still be referenced by the adapter unless
* you call {@link SectionGraphAdapter#deregisterSection(String)} or {@link
* Section#deregister()}.
*/
public Section removeSection(int position) {
Section removed = mSectionGraph.get(position);
mSectionGraph.remove(position);
return removed;
}
void init() {
mSectionLookup = new HashMap<>();
}
void registerSection(String id, Section section) {
mSectionLookup.put(id, section);
}
/**
*
*/
static final class SectionGraphRoot extends Section {
SectionGraphRoot(ItemManager itemManager) {
mItemManager = itemManager;
}
}
static class Registration {
private final String mId;
private final HashMap<String, Section> mSectionLookup;
public Registration(String id, HashMap<String, Section> sectionLookup) {
mId = id;
mSectionLookup = sectionLookup;
}
public void deregister() {
mSectionLookup.remove(mId);
}
}
}
@au-phiware
Copy link

This looks good; using an item graph makes a lot of sense. Actually, it is a proper tree, right? No cycles are allowed? A cyclic graph would cause non-terminating recursion in Node#getPositionInAdapter...

I see a problem with ItemManager, I think it's adding needless complexity. SectionGraphAdapter does not need to maintain a graph of items, the minimum that it needs to know is counts, i.e. the total number of leaves (items and headers) under a node and the number of direct descendant nodes (children count) for a node. As an implementation detail, I would add a flag to indicate that the counts are valid. You can then do away with Item and ItemManager abstractions by making SectionGraphAdapter abstract and relying on subclasses to implement (a new method) onBindViewHolder(VH holder, int[] indexPathToItem) and getItemViewType(int[] indexPathToItem) the translation from position to indexPathToItem is a simple walk of the tree of counts. The tree of counts can be lazily built, again with calls to getItemCount(int[] indexPath). The subclass of SectionGraphAdapter can then choose to store items in a tree, or an array of arrays, or use db queries (would effectively need rollup for the counts).

Anyway, I'd be interested to see what you push.

@au-phiware
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment