| /* |
| * 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 {@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 |
| */ |
| 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 {@link GroupMetadata}. |
| */ |
| private ArrayList<GroupMetadata> mExpGroupMetadataList; |
| |
| /** The number of children from all currently expanded groups */ |
| private int mTotalExpChildrenCount; |
| |
| /** 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 */ |
| private final DataSetObserver mDataSetObserver = new MyDataSetObserver(); |
| |
| /** |
| * Constructs the connector |
| */ |
| public ExpandableListConnector(ExpandableListAdapter expandableListAdapter) { |
| mExpGroupMetadataList = new ArrayList<GroupMetadata>(); |
| |
| setExpandableListAdapter(expandableListAdapter); |
| } |
| |
| /** |
| * 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. |
| * |
| * @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. |
| * |
| * @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. |
| * |
| * @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 |
| * |
| * @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 |
| * @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. |
| * @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 |
| */ |
| 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 |
| * |
| * @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 |
| */ |
| static class GroupMetadata implements Parcelable, Comparable<GroupMetadata> { |
| final static int REFRESH = -1; |
| |
| /** 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 |
| */ |
| int lastChildFlPos; |
| |
| /** |
| * This group's group position |
| */ |
| int gPos; |
| |
| /** |
| * 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.) |
| */ |
| 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) */ |
| 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) |
| */ |
| 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. |
| */ |
| 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) |
| * |
| * @return whether the group at groupPos is expanded or not |
| */ |
| public boolean isExpanded() { |
| return groupMetadata != null; |
| } |
| } |
| } |