ExpandableListConnectorpublic class ExpandableListConnector extends BaseAdapter implements FilterableA {@link BaseAdapter} that provides data/Views in an expandable list (offers
features such as collapsing/expanding groups containing children). By
itself, this adapter has no data and is a connector to a
{@link ExpandableListAdapter} which provides the data.
Internally, this connector translates the flat list position that the
ListAdapter expects to/from group and child positions that the ExpandableListAdapter
expects. |
Fields Summary |
---|
private ExpandableListAdapter | mExpandableListAdapterThe ExpandableListAdapter to fetch the data/Views for this expandable list | private ArrayList | mExpGroupMetadataListList of metadata for the currently expanded groups. The metadata consists
of data essential for efficiently translating between flat list positions
and group/child positions. See {@link GroupMetadata}. | private int | mTotalExpChildrenCountThe number of children from all currently expanded groups | private int | mMaxExpGroupCountThe maximum number of allowable expanded groups. Defaults to 'no limit' | private final android.database.DataSetObserver | mDataSetObserverChange observer used to have ExpandableListAdapter changes pushed to us |
Constructors Summary |
---|
public ExpandableListConnector(ExpandableListAdapter expandableListAdapter)Constructs the connector
mExpGroupMetadataList = new ArrayList<GroupMetadata>();
setExpandableListAdapter(expandableListAdapter);
|
Methods Summary |
---|
public boolean | areAllItemsEnabled()
return mExpandableListAdapter.areAllItemsEnabled();
| boolean | collapseGroup(int groupPos)Collapse a group in the grouped list view
ExpandableListPosition elGroupPos = ExpandableListPosition.obtain(
ExpandableListPosition.GROUP, groupPos, -1, -1);
PositionMetadata pm = getFlattenedPos(elGroupPos);
elGroupPos.recycle();
if (pm == null) return false;
boolean retValue = collapseGroup(pm);
pm.recycle();
return retValue;
| boolean | collapseGroup(android.widget.ExpandableListConnector$PositionMetadata posMetadata)
/*
* Collapsing requires removal from mExpGroupMetadataList
*/
/*
* If it is null, it must be already collapsed. This group metadata
* object should have been set from the search that returned the
* position metadata object.
*/
if (posMetadata.groupMetadata == null) return false;
// Remove the group from the list of expanded groups
mExpGroupMetadataList.remove(posMetadata.groupMetadata);
// Refresh the metadata
refreshExpGroupMetadataList(false, false);
// Notify of change
notifyDataSetChanged();
// Give the callback
mExpandableListAdapter.onGroupCollapsed(posMetadata.groupMetadata.gPos);
return true;
| boolean | expandGroup(int groupPos)Expand a group in the grouped list view
ExpandableListPosition elGroupPos = ExpandableListPosition.obtain(
ExpandableListPosition.GROUP, groupPos, -1, -1);
PositionMetadata pm = getFlattenedPos(elGroupPos);
elGroupPos.recycle();
boolean retValue = expandGroup(pm);
pm.recycle();
return retValue;
| boolean | expandGroup(android.widget.ExpandableListConnector$PositionMetadata posMetadata)
/*
* Expanding requires insertion into the mExpGroupMetadataList
*/
if (posMetadata.position.groupPos < 0) {
// TODO clean exit
throw new RuntimeException("Need group");
}
if (mMaxExpGroupCount == 0) return false;
// Check to see if it's already expanded
if (posMetadata.groupMetadata != null) return false;
/* Restrict number of expanded groups to mMaxExpGroupCount */
if (mExpGroupMetadataList.size() >= mMaxExpGroupCount) {
/* Collapse a group */
// TODO: Collapse something not on the screen instead of the first one?
// TODO: Could write overloaded function to take GroupMetadata to collapse
GroupMetadata collapsedGm = mExpGroupMetadataList.get(0);
int collapsedIndex = mExpGroupMetadataList.indexOf(collapsedGm);
collapseGroup(collapsedGm.gPos);
/* Decrement index if it is after the group we removed */
if (posMetadata.groupInsertIndex > collapsedIndex) {
posMetadata.groupInsertIndex--;
}
}
GroupMetadata expandedGm = GroupMetadata.obtain(
GroupMetadata.REFRESH,
GroupMetadata.REFRESH,
posMetadata.position.groupPos,
mExpandableListAdapter.getGroupId(posMetadata.position.groupPos));
mExpGroupMetadataList.add(posMetadata.groupInsertIndex, expandedGm);
// Refresh the metadata
refreshExpGroupMetadataList(false, false);
// Notify of change
notifyDataSetChanged();
// Give the callback
mExpandableListAdapter.onGroupExpanded(expandedGm.gPos);
return true;
| int | findGroupPosition(long groupIdToMatch, int seedGroupPosition)Searches the expandable list adapter for a group position matching the
given group ID. The search starts at the given seed position and then
alternates between moving up and moving down until 1) we find the right
position, or 2) we run out of time, or 3) we have looked at every
position
int count = mExpandableListAdapter.getGroupCount();
if (count == 0) {
return AdapterView.INVALID_POSITION;
}
// If there isn't a selection don't hunt for it
if (groupIdToMatch == AdapterView.INVALID_ROW_ID) {
return AdapterView.INVALID_POSITION;
}
// Pin seed to reasonable values
seedGroupPosition = Math.max(0, seedGroupPosition);
seedGroupPosition = Math.min(count - 1, seedGroupPosition);
long endTime = SystemClock.uptimeMillis() + AdapterView.SYNC_MAX_DURATION_MILLIS;
long rowId;
// first position scanned so far
int first = seedGroupPosition;
// last position scanned so far
int last = seedGroupPosition;
// True if we should move down on the next iteration
boolean next = false;
// True when we have looked at the first item in the data
boolean hitFirst;
// True when we have looked at the last item in the data
boolean hitLast;
// Get the item ID locally (instead of getItemIdAtPosition), so
// we need the adapter
ExpandableListAdapter adapter = getAdapter();
if (adapter == null) {
return AdapterView.INVALID_POSITION;
}
while (SystemClock.uptimeMillis() <= endTime) {
rowId = adapter.getGroupId(seedGroupPosition);
if (rowId == groupIdToMatch) {
// Found it!
return seedGroupPosition;
}
hitLast = last == count - 1;
hitFirst = first == 0;
if (hitLast && hitFirst) {
// Looked at everything
break;
}
if (hitFirst || (next && !hitLast)) {
// Either we hit the top, or we are trying to move down
last++;
seedGroupPosition = last;
// Try going up next time
next = false;
} else if (hitLast || (!next && !hitFirst)) {
// Either we hit the bottom, or we are trying to move up
first--;
seedGroupPosition = first;
// Try going down next time
next = true;
}
}
return AdapterView.INVALID_POSITION;
| ExpandableListAdapter | getAdapter()
return mExpandableListAdapter;
| public int | getCount()
/*
* Total count for the list view is the number groups plus the
* number of children from currently expanded groups (a value we keep
* cached in this class)
*/
return mExpandableListAdapter.getGroupCount() + mTotalExpChildrenCount;
| java.util.ArrayList | getExpandedGroupMetadataList()
return mExpGroupMetadataList;
| public Filter | getFilter()
ExpandableListAdapter adapter = getAdapter();
if (adapter instanceof Filterable) {
return ((Filterable) adapter).getFilter();
} else {
return null;
}
| android.widget.ExpandableListConnector$PositionMetadata | getFlattenedPos(ExpandableListPosition pos)Translates either a group pos or a child pos (+ group it belongs to) to a
flat list position. If searching for a child and its group is not expanded, this will
return null since the child isn't being shown in the ListView, and hence it has no
position.
final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
final int numExpGroups = egml.size();
/* Binary search variables */
int leftExpGroupIndex = 0;
int rightExpGroupIndex = numExpGroups - 1;
int midExpGroupIndex = 0;
GroupMetadata midExpGm;
if (numExpGroups == 0) {
/*
* There aren't any expanded groups, so flPos must be a group and
* its flPos will be the same as its group pos. The
* insert position is 0 (since the list is empty).
*/
return PositionMetadata.obtain(pos.groupPos, pos.type,
pos.groupPos, pos.childPos, null, 0);
}
/*
* Binary search over the expanded groups to find either the exact
* expanded group (if we're looking for a group) or the group that
* contains the child we're looking for.
*/
while (leftExpGroupIndex <= rightExpGroupIndex) {
midExpGroupIndex = (rightExpGroupIndex - leftExpGroupIndex)/2 + leftExpGroupIndex;
midExpGm = egml.get(midExpGroupIndex);
if (pos.groupPos > midExpGm.gPos) {
/*
* It's after the current middle group, so search right
*/
leftExpGroupIndex = midExpGroupIndex + 1;
} else if (pos.groupPos < midExpGm.gPos) {
/*
* It's before the current middle group, so search left
*/
rightExpGroupIndex = midExpGroupIndex - 1;
} else if (pos.groupPos == midExpGm.gPos) {
/*
* It's this middle group, exact hit
*/
if (pos.type == ExpandableListPosition.GROUP) {
/* If it's a group, give them this matched group's flPos */
return PositionMetadata.obtain(midExpGm.flPos, pos.type,
pos.groupPos, pos.childPos, midExpGm, midExpGroupIndex);
} else if (pos.type == ExpandableListPosition.CHILD) {
/* If it's a child, calculate the flat list pos */
return PositionMetadata.obtain(midExpGm.flPos + pos.childPos
+ 1, pos.type, pos.groupPos, pos.childPos,
midExpGm, midExpGroupIndex);
} else {
return null;
}
}
}
/*
* If we've reached here, it means there was no match in the expanded
* groups, so it must be a collapsed group that they're search for
*/
if (pos.type != ExpandableListPosition.GROUP) {
/* If it isn't a group, return null */
return null;
}
/*
* To figure out exact insertion and prior group positions, we need to
* determine how we broke out of the binary search. We backtrack to see
* this.
*/
if (leftExpGroupIndex > midExpGroupIndex) {
/*
* This would occur in the first conditional, so the flat list
* insertion position is after the left group.
*
* The leftGroupPos is one more than it should be (from the binary
* search loop) so we subtract 1 to get the actual left group. Since
* the insertion point is AFTER the left group, we keep this +1
* value as the insertion point
*/
final GroupMetadata leftExpGm = egml.get(leftExpGroupIndex-1);
final int flPos =
leftExpGm.lastChildFlPos
+ (pos.groupPos - leftExpGm.gPos);
return PositionMetadata.obtain(flPos, pos.type, pos.groupPos,
pos.childPos, null, leftExpGroupIndex);
} else if (rightExpGroupIndex < midExpGroupIndex) {
/*
* This would occur in the second conditional, so the flat list
* insertion position is before the right group. Also, the
* rightGroupPos is one less than it should be (from binary search
* loop), so we increment to it.
*/
final GroupMetadata rightExpGm = egml.get(++rightExpGroupIndex);
final int flPos =
rightExpGm.flPos
- (rightExpGm.gPos - pos.groupPos);
return PositionMetadata.obtain(flPos, pos.type, pos.groupPos,
pos.childPos, null, rightExpGroupIndex);
} else {
return null;
}
| public java.lang.Object | getItem(int flatListPos)
final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
Object retValue;
if (posMetadata.position.type == ExpandableListPosition.GROUP) {
retValue = mExpandableListAdapter
.getGroup(posMetadata.position.groupPos);
} else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
retValue = mExpandableListAdapter.getChild(posMetadata.position.groupPos,
posMetadata.position.childPos);
} else {
// TODO: clean exit
throw new RuntimeException("Flat list position is of unknown type");
}
posMetadata.recycle();
return retValue;
| public long | getItemId(int flatListPos)
final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
final long groupId = mExpandableListAdapter.getGroupId(posMetadata.position.groupPos);
long retValue;
if (posMetadata.position.type == ExpandableListPosition.GROUP) {
retValue = mExpandableListAdapter.getCombinedGroupId(groupId);
} else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
final long childId = mExpandableListAdapter.getChildId(posMetadata.position.groupPos,
posMetadata.position.childPos);
retValue = mExpandableListAdapter.getCombinedChildId(groupId, childId);
} else {
// TODO: clean exit
throw new RuntimeException("Flat list position is of unknown type");
}
posMetadata.recycle();
return retValue;
| public int | getItemViewType(int flatListPos)
final PositionMetadata metadata = getUnflattenedPos(flatListPos);
final ExpandableListPosition pos = metadata.position;
int retValue;
if (mExpandableListAdapter instanceof HeterogeneousExpandableList) {
HeterogeneousExpandableList adapter =
(HeterogeneousExpandableList) mExpandableListAdapter;
if (pos.type == ExpandableListPosition.GROUP) {
retValue = adapter.getGroupType(pos.groupPos);
} else {
final int childType = adapter.getChildType(pos.groupPos, pos.childPos);
retValue = adapter.getGroupTypeCount() + childType;
}
} else {
if (pos.type == ExpandableListPosition.GROUP) {
retValue = 0;
} else {
retValue = 1;
}
}
metadata.recycle();
return retValue;
| android.widget.ExpandableListConnector$PositionMetadata | getUnflattenedPos(int flPos)Translates a flat list position to either a) group pos if the specified
flat list position corresponds to a group, or b) child pos if it
corresponds to a child. Performs a binary search on the expanded
groups list to find the flat list pos if it is an exp group, otherwise
finds where the flat list pos fits in between the exp groups.
/* Keep locally since frequent use */
final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
final int numExpGroups = egml.size();
/* Binary search variables */
int leftExpGroupIndex = 0;
int rightExpGroupIndex = numExpGroups - 1;
int midExpGroupIndex = 0;
GroupMetadata midExpGm;
if (numExpGroups == 0) {
/*
* There aren't any expanded groups (hence no visible children
* either), so flPos must be a group and its group pos will be the
* same as its flPos
*/
return PositionMetadata.obtain(flPos, ExpandableListPosition.GROUP, flPos,
-1, null, 0);
}
/*
* Binary search over the expanded groups to find either the exact
* expanded group (if we're looking for a group) or the group that
* contains the child we're looking for. If we are looking for a
* collapsed group, we will not have a direct match here, but we will
* find the expanded group just before the group we're searching for (so
* then we can calculate the group position of the group we're searching
* for). If there isn't an expanded group prior to the group being
* searched for, then the group being searched for's group position is
* the same as the flat list position (since there are no children before
* it, and all groups before it are collapsed).
*/
while (leftExpGroupIndex <= rightExpGroupIndex) {
midExpGroupIndex =
(rightExpGroupIndex - leftExpGroupIndex) / 2
+ leftExpGroupIndex;
midExpGm = egml.get(midExpGroupIndex);
if (flPos > midExpGm.lastChildFlPos) {
/*
* The flat list position is after the current middle group's
* last child's flat list position, so search right
*/
leftExpGroupIndex = midExpGroupIndex + 1;
} else if (flPos < midExpGm.flPos) {
/*
* The flat list position is before the current middle group's
* flat list position, so search left
*/
rightExpGroupIndex = midExpGroupIndex - 1;
} else if (flPos == midExpGm.flPos) {
/*
* The flat list position is this middle group's flat list
* position, so we've found an exact hit
*/
return PositionMetadata.obtain(flPos, ExpandableListPosition.GROUP,
midExpGm.gPos, -1, midExpGm, midExpGroupIndex);
} else if (flPos <= midExpGm.lastChildFlPos
/* && flPos > midGm.flPos as deduced from previous
* conditions */) {
/* The flat list position is a child of the middle group */
/*
* Subtract the first child's flat list position from the
* specified flat list pos to get the child's position within
* the group
*/
final int childPos = flPos - (midExpGm.flPos + 1);
return PositionMetadata.obtain(flPos, ExpandableListPosition.CHILD,
midExpGm.gPos, childPos, midExpGm, midExpGroupIndex);
}
}
/*
* If we've reached here, it means the flat list position must be a
* group that is not expanded, since otherwise we would have hit it
* in the above search.
*/
/**
* If we are to expand this group later, where would it go in the
* mExpGroupMetadataList ?
*/
int insertPosition = 0;
/** What is its group position in the list of all groups? */
int groupPos = 0;
/*
* To figure out exact insertion and prior group positions, we need to
* determine how we broke out of the binary search. We backtrack
* to see this.
*/
if (leftExpGroupIndex > midExpGroupIndex) {
/*
* This would occur in the first conditional, so the flat list
* insertion position is after the left group. Also, the
* leftGroupPos is one more than it should be (since that broke out
* of our binary search), so we decrement it.
*/
final GroupMetadata leftExpGm = egml.get(leftExpGroupIndex-1);
insertPosition = leftExpGroupIndex;
/*
* Sums the number of groups between the prior exp group and this
* one, and then adds it to the prior group's group pos
*/
groupPos =
(flPos - leftExpGm.lastChildFlPos) + leftExpGm.gPos;
} else if (rightExpGroupIndex < midExpGroupIndex) {
/*
* This would occur in the second conditional, so the flat list
* insertion position is before the right group. Also, the
* rightGroupPos is one less than it should be, so increment it.
*/
final GroupMetadata rightExpGm = egml.get(++rightExpGroupIndex);
insertPosition = rightExpGroupIndex;
/*
* Subtracts this group's flat list pos from the group after's flat
* list position to find out how many groups are in between the two
* groups. Then, subtracts that number from the group after's group
* pos to get this group's pos.
*/
groupPos = rightExpGm.gPos - (rightExpGm.flPos - flPos);
} else {
// TODO: clean exit
throw new RuntimeException("Unknown state");
}
return PositionMetadata.obtain(flPos, ExpandableListPosition.GROUP, groupPos, -1,
null, insertPosition);
| public android.view.View | getView(int flatListPos, android.view.View convertView, android.view.ViewGroup parent)
final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
View retValue;
if (posMetadata.position.type == ExpandableListPosition.GROUP) {
retValue = mExpandableListAdapter.getGroupView(posMetadata.position.groupPos,
posMetadata.isExpanded(), convertView, parent);
} else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
final boolean isLastChild = posMetadata.groupMetadata.lastChildFlPos == flatListPos;
retValue = mExpandableListAdapter.getChildView(posMetadata.position.groupPos,
posMetadata.position.childPos, isLastChild, convertView, parent);
} else {
// TODO: clean exit
throw new RuntimeException("Flat list position is of unknown type");
}
posMetadata.recycle();
return retValue;
| public int | getViewTypeCount()
if (mExpandableListAdapter instanceof HeterogeneousExpandableList) {
HeterogeneousExpandableList adapter =
(HeterogeneousExpandableList) mExpandableListAdapter;
return adapter.getGroupTypeCount() + adapter.getChildTypeCount();
} else {
return 2;
}
| public boolean | hasStableIds()
return mExpandableListAdapter.hasStableIds();
| public boolean | isEmpty()
ExpandableListAdapter adapter = getAdapter();
return adapter != null ? adapter.isEmpty() : true;
| public boolean | isEnabled(int flatListPos)
final PositionMetadata metadata = getUnflattenedPos(flatListPos);
final ExpandableListPosition pos = metadata.position;
boolean retValue;
if (pos.type == ExpandableListPosition.CHILD) {
retValue = mExpandableListAdapter.isChildSelectable(pos.groupPos, pos.childPos);
} else {
// Groups are always selectable
retValue = true;
}
metadata.recycle();
return retValue;
| public boolean | isGroupExpanded(int groupPosition)Whether the given group is currently expanded.
GroupMetadata groupMetadata;
for (int i = mExpGroupMetadataList.size() - 1; i >= 0; i--) {
groupMetadata = mExpGroupMetadataList.get(i);
if (groupMetadata.gPos == groupPosition) {
return true;
}
}
return false;
| private void | refreshExpGroupMetadataList(boolean forceChildrenCountRefresh, boolean syncGroupPositions)Traverses the expanded group metadata list and fills in the flat list
positions.
final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
int egmlSize = egml.size();
int curFlPos = 0;
/* Update child count as we go through */
mTotalExpChildrenCount = 0;
if (syncGroupPositions) {
// We need to check whether any groups have moved positions
boolean positionsChanged = false;
for (int i = egmlSize - 1; i >= 0; i--) {
GroupMetadata curGm = egml.get(i);
int newGPos = findGroupPosition(curGm.gId, curGm.gPos);
if (newGPos != curGm.gPos) {
if (newGPos == AdapterView.INVALID_POSITION) {
// Doh, just remove it from the list of expanded groups
egml.remove(i);
egmlSize--;
}
curGm.gPos = newGPos;
if (!positionsChanged) positionsChanged = true;
}
}
if (positionsChanged) {
// At least one group changed positions, so re-sort
Collections.sort(egml);
}
}
int gChildrenCount;
int lastGPos = 0;
for (int i = 0; i < egmlSize; i++) {
/* Store in local variable since we'll access freq */
GroupMetadata curGm = egml.get(i);
/*
* Get the number of children, try to refrain from calling
* another class's method unless we have to (so do a subtraction)
*/
if ((curGm.lastChildFlPos == GroupMetadata.REFRESH) || forceChildrenCountRefresh) {
gChildrenCount = mExpandableListAdapter.getChildrenCount(curGm.gPos);
} else {
/* Num children for this group is its last child's fl pos minus
* the group's fl pos
*/
gChildrenCount = curGm.lastChildFlPos - curGm.flPos;
}
/* Update */
mTotalExpChildrenCount += gChildrenCount;
/*
* This skips the collapsed groups and increments the flat list
* position (for subsequent exp groups) by accounting for the collapsed
* groups
*/
curFlPos += (curGm.gPos - lastGPos);
lastGPos = curGm.gPos;
/* Update the flat list positions, and the current flat list pos */
curGm.flPos = curFlPos;
curFlPos += gChildrenCount;
curGm.lastChildFlPos = curFlPos;
}
| public void | setExpandableListAdapter(ExpandableListAdapter expandableListAdapter)Point to the {@link ExpandableListAdapter} that will give us data/Views
if (mExpandableListAdapter != null) {
mExpandableListAdapter.unregisterDataSetObserver(mDataSetObserver);
}
mExpandableListAdapter = expandableListAdapter;
expandableListAdapter.registerDataSetObserver(mDataSetObserver);
| void | setExpandedGroupMetadataList(java.util.ArrayList expandedGroupMetadataList)
if ((expandedGroupMetadataList == null) || (mExpandableListAdapter == null)) {
return;
}
// Make sure our current data set is big enough for the previously
// expanded groups, if not, ignore this request
int numGroups = mExpandableListAdapter.getGroupCount();
for (int i = expandedGroupMetadataList.size() - 1; i >= 0; i--) {
if (expandedGroupMetadataList.get(i).gPos >= numGroups) {
// Doh, for some reason the client doesn't have some of the groups
return;
}
}
mExpGroupMetadataList = expandedGroupMetadataList;
refreshExpGroupMetadataList(true, false);
| public void | setMaxExpGroupCount(int maxExpGroupCount)Set the maximum number of groups that can be expanded at any given time
mMaxExpGroupCount = maxExpGroupCount;
|
|