/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.widget;

import android.database.DataSetObserver;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;
import java.util.Collections;

/*
 * Implementation notes:
 * 
 * <p>
 * Terminology:
 * <li> flPos - Flat list position, the position used by ListView
 * <li> gPos - Group position, the position of a group among all the groups
 * <li> cPos - Child position, the position of a child among all the children
 * in a group
 */

A 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 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.

/** * A {@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. * <p> * Internally, this connector translates the flat list position that the * ListAdapter expects to/from group and child positions that the ExpandableListAdapter * expects. */
class ExpandableListConnector extends BaseAdapter implements Filterable {
The ExpandableListAdapter to fetch the data/Views for this expandable list
/** * The ExpandableListAdapter to fetch the data/Views for this expandable list */
private ExpandableListAdapter mExpandableListAdapter;
List 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 GroupMetadata.
/** * List 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 ArrayList<GroupMetadata> mExpGroupMetadataList;
The number of children from all currently expanded groups
/** The number of children from all currently expanded groups */
private int mTotalExpChildrenCount;
The maximum number of allowable expanded groups. Defaults to 'no limit'
/** The maximum number of allowable expanded groups. Defaults to 'no limit' */
private int mMaxExpGroupCount = Integer.MAX_VALUE;
Change observer used to have ExpandableListAdapter changes pushed to us
/** Change observer used to have ExpandableListAdapter changes pushed to us */
private final DataSetObserver mDataSetObserver = new MyDataSetObserver();
Constructs the connector
/** * Constructs the connector */
public ExpandableListConnector(ExpandableListAdapter expandableListAdapter) { mExpGroupMetadataList = new ArrayList<GroupMetadata>(); setExpandableListAdapter(expandableListAdapter); }
Point to the ExpandableListAdapter that will give us data/Views
Params:
  • expandableListAdapter – the adapter that supplies us with data/Views
/** * Point to the {@link ExpandableListAdapter} that will give us data/Views * * @param expandableListAdapter the adapter that supplies us with data/Views */
public void setExpandableListAdapter(ExpandableListAdapter expandableListAdapter) { if (mExpandableListAdapter != null) { mExpandableListAdapter.unregisterDataSetObserver(mDataSetObserver); } mExpandableListAdapter = expandableListAdapter; expandableListAdapter.registerDataSetObserver(mDataSetObserver); }
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.
Params:
  • flPos – the flat list position to be translated
Returns:the group position or child position of the specified flat list position encompassed in a PositionMetadata object that contains additional useful info for insertion, etc.
/** * 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. * * @param flPos the flat list position to be translated * @return the group position or child position of the specified flat list * position encompassed in a {@link PositionMetadata} object * that contains additional useful info for insertion, etc. */
PositionMetadata getUnflattenedPos(final int flPos) { /* 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); }
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.
Params:
Returns:the flat list position encompassed in a PositionMetadata object that contains additional useful info for insertion, etc., or null.
/** * 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. * * @param pos a {@link ExpandableListPosition} representing either a group position * or child position * @return the flat list position encompassed in a {@link PositionMetadata} * object that contains additional useful info for insertion, etc., or null. */
PositionMetadata getFlattenedPos(final ExpandableListPosition pos) { 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; } } @Override public boolean areAllItemsEnabled() { return mExpandableListAdapter.areAllItemsEnabled(); } @Override 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 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; } public 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 View getView(int flatListPos, View convertView, 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; } @Override 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; } @Override public int getViewTypeCount() { if (mExpandableListAdapter instanceof HeterogeneousExpandableList) { HeterogeneousExpandableList adapter = (HeterogeneousExpandableList) mExpandableListAdapter; return adapter.getGroupTypeCount() + adapter.getChildTypeCount(); } else { return 2; } } @Override public boolean hasStableIds() { return mExpandableListAdapter.hasStableIds(); }
Traverses the expanded group metadata list and fills in the flat list positions.
Params:
  • forceChildrenCountRefresh – Forces refreshing of the children count for all expanded groups.
  • syncGroupPositions – Whether to search for the group positions based on the group IDs. This should only be needed when calling this from an onChanged callback.
/** * Traverses the expanded group metadata list and fills in the flat list * positions. * * @param forceChildrenCountRefresh Forces refreshing of the children count * for all expanded groups. * @param syncGroupPositions Whether to search for the group positions * based on the group IDs. This should only be needed when calling * this from an onChanged callback. */
@SuppressWarnings("unchecked") private void refreshExpGroupMetadataList(boolean forceChildrenCountRefresh, boolean syncGroupPositions) { 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; } }
Collapse a group in the grouped list view
Params:
  • groupPos – position of the group to collapse
/** * Collapse a group in the grouped list view * * @param groupPos position of the group to collapse */
boolean collapseGroup(int groupPos) { 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(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; }
Expand a group in the grouped list view
Params:
  • groupPos – the group to be expanded
/** * Expand a group in the grouped list view * @param groupPos the group to be expanded */
boolean expandGroup(int groupPos) { 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(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; }
Whether the given group is currently expanded.
Params:
  • groupPosition – The group to check.
Returns:Whether the group is currently expanded.
/** * Whether the given group is currently expanded. * @param groupPosition The group to check. * @return Whether the group is currently expanded. */
public boolean isGroupExpanded(int groupPosition) { GroupMetadata groupMetadata; for (int i = mExpGroupMetadataList.size() - 1; i >= 0; i--) { groupMetadata = mExpGroupMetadataList.get(i); if (groupMetadata.gPos == groupPosition) { return true; } } return false; }
Set the maximum number of groups that can be expanded at any given time
/** * Set the maximum number of groups that can be expanded at any given time */
public void setMaxExpGroupCount(int maxExpGroupCount) { mMaxExpGroupCount = maxExpGroupCount; } ExpandableListAdapter getAdapter() { return mExpandableListAdapter; } public Filter getFilter() { ExpandableListAdapter adapter = getAdapter(); if (adapter instanceof Filterable) { return ((Filterable) adapter).getFilter(); } else { return null; } } ArrayList<GroupMetadata> getExpandedGroupMetadataList() { return mExpGroupMetadataList; } void setExpandedGroupMetadataList(ArrayList<GroupMetadata> 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); } @Override public boolean isEmpty() { ExpandableListAdapter adapter = getAdapter(); return adapter != null ? adapter.isEmpty() : true; }
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
See Also:
Returns:Position of the row that matches the given row ID, or AdapterView.INVALID_POSITION if it can't be found
/** * 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 * * @return Position of the row that matches the given row ID, or * {@link AdapterView#INVALID_POSITION} if it can't be found * @see AdapterView#findSyncPosition() */
int findGroupPosition(long groupIdToMatch, int seedGroupPosition) { 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; } protected class MyDataSetObserver extends DataSetObserver { @Override public void onChanged() { refreshExpGroupMetadataList(true, true); notifyDataSetChanged(); } @Override public void onInvalidated() { refreshExpGroupMetadataList(true, true); notifyDataSetInvalidated(); } }
Metadata about an expanded group to help convert from a flat list position to either a) group position for groups, or b) child position for children
/** * Metadata about an expanded group to help convert from a flat list * position to either a) group position for groups, or b) child position for * children */
static class GroupMetadata implements Parcelable, Comparable<GroupMetadata> { final static int REFRESH = -1;
This group's flat list position
/** This group's flat list position */
int flPos; /* firstChildFlPos isn't needed since it's (flPos + 1) */
This group's last child's flat list position, so basically the range of this group in the flat list
/** * This group's last child's flat list position, so basically * the range of this group in the flat list */
int lastChildFlPos;
This group's group position
/** * This group's group position */
int gPos;
This group's id
/** * This group's id */
long gId; private GroupMetadata() { } static GroupMetadata obtain(int flPos, int lastChildFlPos, int gPos, long gId) { GroupMetadata gm = new GroupMetadata(); gm.flPos = flPos; gm.lastChildFlPos = lastChildFlPos; gm.gPos = gPos; gm.gId = gId; return gm; } public int compareTo(GroupMetadata another) { if (another == null) { throw new IllegalArgumentException(); } return gPos - another.gPos; } public int describeContents() { return 0; } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(flPos); dest.writeInt(lastChildFlPos); dest.writeInt(gPos); dest.writeLong(gId); } public static final Parcelable.Creator<GroupMetadata> CREATOR = new Parcelable.Creator<GroupMetadata>() { public GroupMetadata createFromParcel(Parcel in) { GroupMetadata gm = GroupMetadata.obtain( in.readInt(), in.readInt(), in.readInt(), in.readLong()); return gm; } public GroupMetadata[] newArray(int size) { return new GroupMetadata[size]; } }; }
Data type that contains an expandable list position (can refer to either a group or child) and some extra information regarding referred item (such as where to insert into the flat list, etc.)
/** * Data type that contains an expandable list position (can refer to either a group * or child) and some extra information regarding referred item (such as * where to insert into the flat list, etc.) */
static public class PositionMetadata { private static final int MAX_POOL_SIZE = 5; private static ArrayList<PositionMetadata> sPool = new ArrayList<PositionMetadata>(MAX_POOL_SIZE);
Data type to hold the position and its type (child/group)
/** Data type to hold the position and its type (child/group) */
public ExpandableListPosition position;
Link back to the expanded GroupMetadata for this group. Useful for removing the group from the list of expanded groups inside the connector when we collapse the group, and also as a check to see if the group was expanded or collapsed (this will be null if the group is collapsed since we don't keep that group's metadata)
/** * Link back to the expanded GroupMetadata for this group. Useful for * removing the group from the list of expanded groups inside the * connector when we collapse the group, and also as a check to see if * the group was expanded or collapsed (this will be null if the group * is collapsed since we don't keep that group's metadata) */
public GroupMetadata groupMetadata;
For groups that are collapsed, we use this as the index (in mExpGroupMetadataList) to insert this group when we are expanding this group.
/** * For groups that are collapsed, we use this as the index (in * mExpGroupMetadataList) to insert this group when we are expanding * this group. */
public int groupInsertIndex; private void resetState() { if (position != null) { position.recycle(); position = null; } groupMetadata = null; groupInsertIndex = 0; } /** * Use {@link #obtain(int, int, int, int, GroupMetadata, int)} */ private PositionMetadata() { } static PositionMetadata obtain(int flatListPos, int type, int groupPos, int childPos, GroupMetadata groupMetadata, int groupInsertIndex) { PositionMetadata pm = getRecycledOrCreate(); pm.position = ExpandableListPosition.obtain(type, groupPos, childPos, flatListPos); pm.groupMetadata = groupMetadata; pm.groupInsertIndex = groupInsertIndex; return pm; } private static PositionMetadata getRecycledOrCreate() { PositionMetadata pm; synchronized (sPool) { if (sPool.size() > 0) { pm = sPool.remove(0); } else { return new PositionMetadata(); } } pm.resetState(); return pm; } public void recycle() { resetState(); synchronized (sPool) { if (sPool.size() < MAX_POOL_SIZE) { sPool.add(this); } } }
Checks whether the group referred to in this object is expanded, or not (at the time this object was created)
Returns:whether the group at groupPos is expanded or not
/** * Checks whether the group referred to in this object is expanded, * or not (at the time this object was created) * * @return whether the group at groupPos is expanded or not */
public boolean isExpanded() { return groupMetadata != null; } } }