diff --git a/src/api-impl/android/view/SoundEffectConstants.java b/src/api-impl/android/view/SoundEffectConstants.java index dc1752b1..177f54ab 100644 --- a/src/api-impl/android/view/SoundEffectConstants.java +++ b/src/api-impl/android/view/SoundEffectConstants.java @@ -2,6 +2,8 @@ package android.view; public class SoundEffectConstants { + public static final int CLICK = 0; + // the typo is part of the API public static int getContantForFocusDirection(int direction) { return 0; diff --git a/src/api-impl/android/widget/BaseExpandableListAdapter.java b/src/api-impl/android/widget/BaseExpandableListAdapter.java new file mode 100644 index 00000000..a9bec532 --- /dev/null +++ b/src/api-impl/android/widget/BaseExpandableListAdapter.java @@ -0,0 +1,137 @@ +/* + * 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.DataSetObservable; +import android.database.DataSetObserver; + +/** + * Base class for a {@link ExpandableListAdapter} used to provide data and Views + * from some data to an expandable list view. + *

+ * Adapters inheriting this class should verify that the base implementations of + * {@link #getCombinedChildId(long, long)} and {@link #getCombinedGroupId(long)} + * are correct in generating unique IDs from the group/children IDs. + *

+ * @see SimpleExpandableListAdapter + * @see SimpleCursorTreeAdapter + */ +public abstract class BaseExpandableListAdapter implements ExpandableListAdapter, + HeterogeneousExpandableList { + private final DataSetObservable mDataSetObservable = new DataSetObservable(); + + public void registerDataSetObserver(DataSetObserver observer) { + mDataSetObservable.registerObserver(observer); + } + + public void unregisterDataSetObserver(DataSetObserver observer) { + mDataSetObservable.unregisterObserver(observer); + } + + /** + * @see DataSetObservable#notifyInvalidated() + */ + public void notifyDataSetInvalidated() { + mDataSetObservable.notifyInvalidated(); + } + + /** + * @see DataSetObservable#notifyChanged() + */ + public void notifyDataSetChanged() { + mDataSetObservable.notifyChanged(); + } + + public boolean areAllItemsEnabled() { + return true; + } + + public void onGroupCollapsed(int groupPosition) { + } + + public void onGroupExpanded(int groupPosition) { + } + + /** + * Override this method if you foresee a clash in IDs based on this scheme: + *

+ * Base implementation returns a long: + *

  • bit 0: Whether this ID points to a child (unset) or group (set), so for this method + * this bit will be 1. + *
  • bit 1-31: Lower 31 bits of the groupId + *
  • bit 32-63: Lower 32 bits of the childId. + *

    + * {@inheritDoc} + */ + public long getCombinedChildId(long groupId, long childId) { + return 0x8000000000000000L | ((groupId & 0x7FFFFFFF) << 32) | (childId & 0xFFFFFFFF); + } + + /** + * Override this method if you foresee a clash in IDs based on this scheme: + *

    + * Base implementation returns a long: + *

  • bit 0: Whether this ID points to a child (unset) or group (set), so for this method + * this bit will be 0. + *
  • bit 1-31: Lower 31 bits of the groupId + *
  • bit 32-63: Lower 32 bits of the childId. + *

    + * {@inheritDoc} + */ + public long getCombinedGroupId(long groupId) { + return (groupId & 0x7FFFFFFF) << 32; + } + + /** + * {@inheritDoc} + */ + /*public boolean isEmpty() { + return getGroupCount() == 0; + }*/ + + /** + * {@inheritDoc} + * @return 0 for any group or child position, since only one child type count is declared. + */ + public int getChildType(int groupPosition, int childPosition) { + return 0; + } + + /** + * {@inheritDoc} + * @return 1 as a default value in BaseExpandableListAdapter. + */ + public int getChildTypeCount() { + return 1; + } + + /** + * {@inheritDoc} + * @return 0 for any groupPosition, since only one group type count is declared. + */ + public int getGroupType(int groupPosition) { + return 0; + } + + /** + * {@inheritDoc} + * @return 1 as a default value in BaseExpandableListAdapter. + */ + public int getGroupTypeCount() { + return 1; + } +} diff --git a/src/api-impl/android/widget/ExpandableListAdapter.java b/src/api-impl/android/widget/ExpandableListAdapter.java new file mode 100644 index 00000000..6ead7451 --- /dev/null +++ b/src/api-impl/android/widget/ExpandableListAdapter.java @@ -0,0 +1,209 @@ +/* + * 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.view.View; +import android.view.ViewGroup; + +/** + * An adapter that links a {@link ExpandableListView} with the underlying + * data. The implementation of this interface will provide access + * to the data of the children (categorized by groups), and also instantiate + * {@link View}s for children and groups. + */ +public interface ExpandableListAdapter { + /** + * @see Adapter#registerDataSetObserver(DataSetObserver) + */ + void registerDataSetObserver(DataSetObserver observer); + + /** + * @see Adapter#unregisterDataSetObserver(DataSetObserver) + */ + void unregisterDataSetObserver(DataSetObserver observer); + + /** + * Gets the number of groups. + * + * @return the number of groups + */ + int getGroupCount(); + + /** + * Gets the number of children in a specified group. + * + * @param groupPosition the position of the group for which the children + * count should be returned + * @return the children count in the specified group + */ + int getChildrenCount(int groupPosition); + + /** + * Gets the data associated with the given group. + * + * @param groupPosition the position of the group + * @return the data child for the specified group + */ + Object getGroup(int groupPosition); + + /** + * Gets the data associated with the given child within the given group. + * + * @param groupPosition the position of the group that the child resides in + * @param childPosition the position of the child with respect to other + * children in the group + * @return the data of the child + */ + Object getChild(int groupPosition, int childPosition); + + /** + * Gets the ID for the group at the given position. This group ID must be + * unique across groups. The combined ID (see + * {@link #getCombinedGroupId(long)}) must be unique across ALL items + * (groups and all children). + * + * @param groupPosition the position of the group for which the ID is wanted + * @return the ID associated with the group + */ + long getGroupId(int groupPosition); + + /** + * Gets the ID for the given child within the given group. This ID must be + * unique across all children within the group. The combined ID (see + * {@link #getCombinedChildId(long, long)}) must be unique across ALL items + * (groups and all children). + * + * @param groupPosition the position of the group that contains the child + * @param childPosition the position of the child within the group for which + * the ID is wanted + * @return the ID associated with the child + */ + long getChildId(int groupPosition, int childPosition); + + /** + * Indicates whether the child and group IDs are stable across changes to the + * underlying data. + * + * @return whether or not the same ID always refers to the same object + * @see Adapter#hasStableIds() + */ + boolean hasStableIds(); + + /** + * Gets a View that displays the given group. This View is only for the + * group--the Views for the group's children will be fetched using + * {@link #getChildView(int, int, boolean, View, ViewGroup)}. + * + * @param groupPosition the position of the group for which the View is + * returned + * @param isExpanded whether the group is expanded or collapsed + * @param convertView the old view to reuse, if possible. You should check + * that this view is non-null and of an appropriate type before + * using. If it is not possible to convert this view to display + * the correct data, this method can create a new view. It is not + * guaranteed that the convertView will have been previously + * created by + * {@link #getGroupView(int, boolean, View, ViewGroup)}. + * @param parent the parent that this view will eventually be attached to + * @return the View corresponding to the group at the specified position + */ + View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent); + + /** + * Gets a View that displays the data for the given child within the given + * group. + * + * @param groupPosition the position of the group that contains the child + * @param childPosition the position of the child (for which the View is + * returned) within the group + * @param isLastChild Whether the child is the last child within the group + * @param convertView the old view to reuse, if possible. You should check + * that this view is non-null and of an appropriate type before + * using. If it is not possible to convert this view to display + * the correct data, this method can create a new view. It is not + * guaranteed that the convertView will have been previously + * created by + * {@link #getChildView(int, int, boolean, View, ViewGroup)}. + * @param parent the parent that this view will eventually be attached to + * @return the View corresponding to the child at the specified position + */ + View getChildView(int groupPosition, int childPosition, boolean isLastChild, + View convertView, ViewGroup parent); + + /** + * Whether the child at the specified position is selectable. + * + * @param groupPosition the position of the group that contains the child + * @param childPosition the position of the child within the group + * @return whether the child is selectable. + */ + boolean isChildSelectable(int groupPosition, int childPosition); + + /** + * @see ListAdapter#areAllItemsEnabled() + */ + boolean areAllItemsEnabled(); + + /** + * @see ListAdapter#isEmpty() + */ + boolean isEmpty(); + + /** + * Called when a group is expanded. + * + * @param groupPosition The group being expanded. + */ + void onGroupExpanded(int groupPosition); + + /** + * Called when a group is collapsed. + * + * @param groupPosition The group being collapsed. + */ + void onGroupCollapsed(int groupPosition); + + /** + * Gets an ID for a child that is unique across any item (either group or + * child) that is in this list. Expandable lists require each item (group or + * child) to have a unique ID among all children and groups in the list. + * This method is responsible for returning that unique ID given a child's + * ID and its group's ID. Furthermore, if {@link #hasStableIds()} is true, the + * returned ID must be stable as well. + * + * @param groupId The ID of the group that contains this child. + * @param childId The ID of the child. + * @return The unique (and possibly stable) ID of the child across all + * groups and children in this list. + */ + long getCombinedChildId(long groupId, long childId); + + /** + * Gets an ID for a group that is unique across any item (either group or + * child) that is in this list. Expandable lists require each item (group or + * child) to have a unique ID among all children and groups in the list. + * This method is responsible for returning that unique ID given a group's + * ID. Furthermore, if {@link #hasStableIds()} is true, the returned ID must be + * stable as well. + * + * @param groupId The ID of the group + * @return The unique (and possibly stable) ID of the group across all + * groups and children in this list. + */ + long getCombinedGroupId(long groupId); +} diff --git a/src/api-impl/android/widget/ExpandableListConnector.java b/src/api-impl/android/widget/ExpandableListConnector.java new file mode 100644 index 00000000..a44d78bc --- /dev/null +++ b/src/api-impl/android/widget/ExpandableListConnector.java @@ -0,0 +1,1044 @@ +/* + * 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: + * + *

    + * Terminology: + *

  • flPos - Flat list position, the position used by ListView + *
  • gPos - Group position, the position of a group among all the groups + *
  • 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. + *

    + * 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 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(); + + 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 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 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 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 getExpandedGroupMetadataList() { + return mExpGroupMetadataList; + } + + void setExpandedGroupMetadataList(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); + } + + @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 { + 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 @android.annotation.NonNull Parcelable.Creator CREATOR = null + /*new Parcelable.Creator() { + 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 sPool = + new ArrayList(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; + } + } +} diff --git a/src/api-impl/android/widget/ExpandableListPosition.java b/src/api-impl/android/widget/ExpandableListPosition.java new file mode 100644 index 00000000..3962346b --- /dev/null +++ b/src/api-impl/android/widget/ExpandableListPosition.java @@ -0,0 +1,141 @@ +/* + * 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 java.util.ArrayList; + +/** + * ExpandableListPosition can refer to either a group's position or a child's + * position. Referring to a child's position requires both a group position (the + * group containing the child) and a child position (the child's position within + * that group). To create objects, use {@link #obtainChildPosition(int, int)} or + * {@link #obtainGroupPosition(int)}. + */ +class ExpandableListPosition { + + private static final int MAX_POOL_SIZE = 5; + private static ArrayList sPool = + new ArrayList(MAX_POOL_SIZE); + + /** + * This data type represents a child position + */ + public final static int CHILD = 1; + + /** + * This data type represents a group position + */ + public final static int GROUP = 2; + + /** + * The position of either the group being referred to, or the parent + * group of the child being referred to + */ + public int groupPos; + + /** + * The position of the child within its parent group + */ + public int childPos; + + /** + * The position of the item in the flat list (optional, used internally when + * the corresponding flat list position for the group or child is known) + */ + int flatListPos; + + /** + * What type of position this ExpandableListPosition represents + */ + public int type; + + private void resetState() { + groupPos = 0; + childPos = 0; + flatListPos = 0; + type = 0; + } + + private ExpandableListPosition() { + } + + long getPackedPosition() { + if (type == CHILD) + return ExpandableListView.getPackedPositionForChild(groupPos, childPos); + else + return ExpandableListView.getPackedPositionForGroup(groupPos); + } + + static ExpandableListPosition obtainGroupPosition(int groupPosition) { + return obtain(GROUP, groupPosition, 0, 0); + } + + static ExpandableListPosition obtainChildPosition(int groupPosition, int childPosition) { + return obtain(CHILD, groupPosition, childPosition, 0); + } + + static ExpandableListPosition obtainPosition(long packedPosition) { + if (packedPosition == ExpandableListView.PACKED_POSITION_VALUE_NULL) { + return null; + } + + ExpandableListPosition elp = getRecycledOrCreate(); + elp.groupPos = ExpandableListView.getPackedPositionGroup(packedPosition); + if (ExpandableListView.getPackedPositionType(packedPosition) == + ExpandableListView.PACKED_POSITION_TYPE_CHILD) { + elp.type = CHILD; + elp.childPos = ExpandableListView.getPackedPositionChild(packedPosition); + } else { + elp.type = GROUP; + } + return elp; + } + + static ExpandableListPosition obtain(int type, int groupPos, int childPos, int flatListPos) { + ExpandableListPosition elp = getRecycledOrCreate(); + elp.type = type; + elp.groupPos = groupPos; + elp.childPos = childPos; + elp.flatListPos = flatListPos; + return elp; + } + + private static ExpandableListPosition getRecycledOrCreate() { + ExpandableListPosition elp; + synchronized (sPool) { + if (sPool.size() > 0) { + elp = sPool.remove(0); + } else { + return new ExpandableListPosition(); + } + } + elp.resetState(); + return elp; + } + + /** + * Do not call this unless you obtained this via ExpandableListPosition.obtain(). + * PositionMetadata will handle recycling its own children. + */ + public void recycle() { + synchronized (sPool) { + if (sPool.size() < MAX_POOL_SIZE) { + sPool.add(this); + } + } + } +} diff --git a/src/api-impl/android/widget/ExpandableListView.java b/src/api-impl/android/widget/ExpandableListView.java index 3da05238..8ca493a2 100644 --- a/src/api-impl/android/widget/ExpandableListView.java +++ b/src/api-impl/android/widget/ExpandableListView.java @@ -1,26 +1,1435 @@ +/* + * Copyright (C) 2006 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.content.Context; -import android.util.AttributeSet; -import android.view.View; +import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; +//import android.compat.annotation.UnsupportedAppUsage; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +//import android.view.ContextMenu; +//import android.view.ContextMenu.ContextMenuInfo; +import android.view.SoundEffectConstants; +import android.view.View; +//import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.ExpandableListConnector.PositionMetadata; +import com.android.internal.R; +import java.util.ArrayList; + +/** + * A view that shows items in a vertically scrolling two-level list. This + * differs from the {@link ListView} by allowing two levels: groups which can + * individually be expanded to show its children. The items come from the + * {@link ExpandableListAdapter} associated with this view. + *

    + * Expandable lists are able to show an indicator beside each item to display + * the item's current state (the states are usually one of expanded group, + * collapsed group, child, or last child). Use + * {@link #setChildIndicator(Drawable)} or {@link #setGroupIndicator(Drawable)} + * (or the corresponding XML attributes) to set these indicators (see the docs + * for each method to see additional state that each Drawable can have). The + * default style for an {@link ExpandableListView} provides indicators which + * will be shown next to Views given to the {@link ExpandableListView}. The + * layouts android.R.layout.simple_expandable_list_item_1 and + * android.R.layout.simple_expandable_list_item_2 (which should be used with + * {@link SimpleCursorTreeAdapter}) contain the preferred position information + * for indicators. + *

    + * The context menu information set by an {@link ExpandableListView} will be a + * {@link ExpandableListContextMenuInfo} object with + * {@link ExpandableListContextMenuInfo#packedPosition} being a packed position + * that can be used with {@link #getPackedPositionType(long)} and the other + * similar methods. + *

    + * Note: You cannot use the value wrap_content + * for the android:layout_height attribute of a + * ExpandableListView in XML if the parent's size is also not strictly specified + * (for example, if the parent were ScrollView you could not specify + * wrap_content since it also can be any length. However, you can use + * wrap_content if the ExpandableListView parent has a specific size, such as + * 100 pixels. + * + * @attr ref android.R.styleable#ExpandableListView_groupIndicator + * @attr ref android.R.styleable#ExpandableListView_indicatorLeft + * @attr ref android.R.styleable#ExpandableListView_indicatorRight + * @attr ref android.R.styleable#ExpandableListView_childIndicator + * @attr ref android.R.styleable#ExpandableListView_childIndicatorLeft + * @attr ref android.R.styleable#ExpandableListView_childIndicatorRight + * @attr ref android.R.styleable#ExpandableListView_childDivider + * @attr ref android.R.styleable#ExpandableListView_indicatorStart + * @attr ref android.R.styleable#ExpandableListView_indicatorEnd + * @attr ref android.R.styleable#ExpandableListView_childIndicatorStart + * @attr ref android.R.styleable#ExpandableListView_childIndicatorEnd + */ public class ExpandableListView extends ListView { + /** + * The packed position represents a group. + */ + public static final int PACKED_POSITION_TYPE_GROUP = 0; + + /** + * The packed position represents a child. + */ + public static final int PACKED_POSITION_TYPE_CHILD = 1; + + /** + * The packed position represents a neither/null/no preference. + */ + public static final int PACKED_POSITION_TYPE_NULL = 2; + + /** + * The value for a packed position that represents neither/null/no + * preference. This value is not otherwise possible since a group type + * (first bit 0) should not have a child position filled. + */ + public static final long PACKED_POSITION_VALUE_NULL = 0x00000000FFFFFFFFL; + + /** + * The mask (in packed position representation) for the child + */ + private static final long PACKED_POSITION_MASK_CHILD = 0x00000000FFFFFFFFL; + + /** + * The mask (in packed position representation) for the group + */ + private static final long PACKED_POSITION_MASK_GROUP = 0x7FFFFFFF00000000L; + + /** + * The mask (in packed position representation) for the type + */ + private static final long PACKED_POSITION_MASK_TYPE = 0x8000000000000000L; + + /** + * The shift amount (in packed position representation) for the group + */ + private static final long PACKED_POSITION_SHIFT_GROUP = 32; + + /** + * The shift amount (in packed position representation) for the type + */ + private static final long PACKED_POSITION_SHIFT_TYPE = 63; + + /** + * The mask (in integer child position representation) for the child + */ + private static final long PACKED_POSITION_INT_MASK_CHILD = 0xFFFFFFFF; + + /** + * The mask (in integer group position representation) for the group + */ + private static final long PACKED_POSITION_INT_MASK_GROUP = 0x7FFFFFFF; + + /** + * Serves as the glue/translator between a ListView and an ExpandableListView + */ + //@UnsupportedAppUsage + private ExpandableListConnector mConnector; + + /** + * Gives us Views through group+child positions + */ + private ExpandableListAdapter mAdapter; + + /** + * Left bound for drawing the indicator. + */ + //@UnsupportedAppUsage + private int mIndicatorLeft; + + /** + * Right bound for drawing the indicator. + */ + //@UnsupportedAppUsage + private int mIndicatorRight; + + /** + * Start bound for drawing the indicator. + */ + private int mIndicatorStart; + + /** + * End bound for drawing the indicator. + */ + private int mIndicatorEnd; + + /** + * Left bound for drawing the indicator of a child. Value of + * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorLeft. + */ + private int mChildIndicatorLeft; + + /** + * Right bound for drawing the indicator of a child. Value of + * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorRight. + */ + private int mChildIndicatorRight; + + /** + * Start bound for drawing the indicator of a child. Value of + * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorStart. + */ + private int mChildIndicatorStart; + + /** + * End bound for drawing the indicator of a child. Value of + * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorEnd. + */ + private int mChildIndicatorEnd; + + /** + * Denotes when a child indicator should inherit this bound from the generic + * indicator bounds + */ + public static final int CHILD_INDICATOR_INHERIT = -1; + + /** + * Denotes an undefined value for an indicator + */ + private static final int INDICATOR_UNDEFINED = -2; + + /** + * The indicator drawn next to a group. + */ + //@UnsupportedAppUsage + private Drawable mGroupIndicator; + + /** + * The indicator drawn next to a child. + */ + private Drawable mChildIndicator; + + private static final int[] EMPTY_STATE_SET = {}; + + /** + * State indicating the group is expanded. + */ + private static final int[] GROUP_EXPANDED_STATE_SET = + {R.attr.state_expanded}; + + /** + * State indicating the group is empty (has no children). + */ + private static final int[] GROUP_EMPTY_STATE_SET = + {R.attr.state_empty}; + + /** + * State indicating the group is expanded and empty (has no children). + */ + private static final int[] GROUP_EXPANDED_EMPTY_STATE_SET = + {R.attr.state_expanded, R.attr.state_empty}; + + /** + * States for the group where the 0th bit is expanded and 1st bit is empty. + */ + //@UnsupportedAppUsage + private static final int[][] GROUP_STATE_SETS = { + EMPTY_STATE_SET, // 00 + GROUP_EXPANDED_STATE_SET, // 01 + GROUP_EMPTY_STATE_SET, // 10 + GROUP_EXPANDED_EMPTY_STATE_SET // 11 + }; + + /** + * State indicating the child is the last within its group. + */ + private static final int[] CHILD_LAST_STATE_SET = + {R.attr.state_last}; + + /** + * Drawable to be used as a divider when it is adjacent to any children + */ + //@UnsupportedAppUsage + private Drawable mChildDivider; + + // Bounds of the indicator to be drawn + private final Rect mIndicatorRect = new Rect(); + public ExpandableListView(Context context) { - super(context); + this(context, null); } - public ExpandableListView(Context context, AttributeSet attributeSet) { - super(context, attributeSet); + public ExpandableListView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.expandableListViewStyle); } + public ExpandableListView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ExpandableListView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.ExpandableListView, defStyleAttr, defStyleRes); + saveAttributeDataForStyleable(context, com.android.internal.R.styleable.ExpandableListView, + attrs, a, defStyleAttr, defStyleRes); + + mGroupIndicator = a.getDrawable( + com.android.internal.R.styleable.ExpandableListView_groupIndicator); + mChildIndicator = a.getDrawable( + com.android.internal.R.styleable.ExpandableListView_childIndicator); + mIndicatorLeft = a.getDimensionPixelSize( + com.android.internal.R.styleable.ExpandableListView_indicatorLeft, 0); + mIndicatorRight = a.getDimensionPixelSize( + com.android.internal.R.styleable.ExpandableListView_indicatorRight, 0); + if (mIndicatorRight == 0 && mGroupIndicator != null) { + mIndicatorRight = mIndicatorLeft + mGroupIndicator.getIntrinsicWidth(); + } + mChildIndicatorLeft = a.getDimensionPixelSize( + com.android.internal.R.styleable.ExpandableListView_childIndicatorLeft, + CHILD_INDICATOR_INHERIT); + mChildIndicatorRight = a.getDimensionPixelSize( + com.android.internal.R.styleable.ExpandableListView_childIndicatorRight, + CHILD_INDICATOR_INHERIT); + mChildDivider = a.getDrawable( + com.android.internal.R.styleable.ExpandableListView_childDivider); + + if (!isRtlCompatibilityMode()) { + mIndicatorStart = a.getDimensionPixelSize( + com.android.internal.R.styleable.ExpandableListView_indicatorStart, + INDICATOR_UNDEFINED); + mIndicatorEnd = a.getDimensionPixelSize( + com.android.internal.R.styleable.ExpandableListView_indicatorEnd, + INDICATOR_UNDEFINED); + + mChildIndicatorStart = a.getDimensionPixelSize( + com.android.internal.R.styleable.ExpandableListView_childIndicatorStart, + CHILD_INDICATOR_INHERIT); + mChildIndicatorEnd = a.getDimensionPixelSize( + com.android.internal.R.styleable.ExpandableListView_childIndicatorEnd, + CHILD_INDICATOR_INHERIT); + } + + a.recycle(); + } + + /** + * Return true if we are in RTL compatibility mode (either before Jelly Bean MR1 or + * RTL not supported) + */ + private boolean isRtlCompatibilityMode() { + //final int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion; + //return targetSdkVersion < JELLY_BEAN_MR1 || !hasRtlSupport(); + return true; + } + + /** + * Return true if the application tag in the AndroidManifest has set "supportRtl" to true + */ + private boolean hasRtlSupport() { + //return mContext.getApplicationInfo().hasRtlSupport(); + return false; + } + + public void onRtlPropertiesChanged(int layoutDirection) { + resolveIndicator(); + resolveChildIndicator(); + } + + /** + * Resolve start/end indicator. start/end indicator always takes precedence over left/right + * indicator when defined. + */ + private void resolveIndicator() { + final boolean isLayoutRtl = false/*isLayoutRtl()*/; + if (isLayoutRtl) { + if (mIndicatorStart >= 0) { + mIndicatorRight = mIndicatorStart; + } + if (mIndicatorEnd >= 0) { + mIndicatorLeft = mIndicatorEnd; + } + } else { + if (mIndicatorStart >= 0) { + mIndicatorLeft = mIndicatorStart; + } + if (mIndicatorEnd >= 0) { + mIndicatorRight = mIndicatorEnd; + } + } + if (mIndicatorRight == 0 && mGroupIndicator != null) { + mIndicatorRight = mIndicatorLeft + mGroupIndicator.getIntrinsicWidth(); + } + } + + /** + * Resolve start/end child indicator. start/end child indicator always takes precedence over + * left/right child indicator when defined. + */ + private void resolveChildIndicator() { + final boolean isLayoutRtl = false/*isLayoutRtl()*/; + if (isLayoutRtl) { + if (mChildIndicatorStart >= CHILD_INDICATOR_INHERIT) { + mChildIndicatorRight = mChildIndicatorStart; + } + if (mChildIndicatorEnd >= CHILD_INDICATOR_INHERIT) { + mChildIndicatorLeft = mChildIndicatorEnd; + } + } else { + if (mChildIndicatorStart >= CHILD_INDICATOR_INHERIT) { + mChildIndicatorLeft = mChildIndicatorStart; + } + if (mChildIndicatorEnd >= CHILD_INDICATOR_INHERIT) { + mChildIndicatorRight = mChildIndicatorEnd; + } + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + // Draw children, etc. + super.dispatchDraw(canvas); + + // If we have any indicators to draw, we do it here + if ((mChildIndicator == null) && (mGroupIndicator == null)) { + return; + } + + int saveCount = 0; + final boolean clipToPadding = false/*(mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK*/; + if (clipToPadding) { + saveCount = canvas.save(); + final int scrollX = getScrollX(); + final int scrollY = getScrollY(); + canvas.clipRect(scrollX + getPaddingLeft(), scrollY + getPaddingTop(), + scrollX + getRight() - getLeft() - getPaddingRight(), + scrollY + getBottom() - getTop() - getPaddingBottom()); + } + + final int headerViewsCount = getHeaderViewsCount(); + + final int lastChildFlPos = mItemCount - getFooterViewsCount() - headerViewsCount - 1; + + final int myB = getBottom(); + + PositionMetadata pos; + View item; + Drawable indicator; + int t, b; + + // Start at a value that is neither child nor group + int lastItemType = ~(ExpandableListPosition.CHILD | ExpandableListPosition.GROUP); + + final Rect indicatorRect = mIndicatorRect; + + // The "child" mentioned in the following two lines is this + // View's child, not referring to an expandable list's + // notion of a child (as opposed to a group) + final int childCount = getChildCount(); + for (int i = 0, childFlPos = mFirstPosition - headerViewsCount; i < childCount; + i++, childFlPos++) { + + if (childFlPos < 0) { + // This child is header + continue; + } else if (childFlPos > lastChildFlPos) { + // This child is footer, so are all subsequent children + break; + } + + item = getChildAt(i); + t = item.getTop(); + b = item.getBottom(); + + // This item isn't on the screen + if ((b < 0) || (t > myB)) + continue; + + // Get more expandable list-related info for this item + pos = mConnector.getUnflattenedPos(childFlPos); + + final boolean isLayoutRtl = false/*isLayoutRtl()*/; + final int width = getWidth(); + + // If this item type and the previous item type are different, then we need to change + // the left & right bounds + if (pos.position.type != lastItemType) { + if (pos.position.type == ExpandableListPosition.CHILD) { + indicatorRect.left = (mChildIndicatorLeft == CHILD_INDICATOR_INHERIT) ? mIndicatorLeft : mChildIndicatorLeft; + indicatorRect.right = (mChildIndicatorRight == CHILD_INDICATOR_INHERIT) ? mIndicatorRight : mChildIndicatorRight; + } else { + indicatorRect.left = mIndicatorLeft; + indicatorRect.right = mIndicatorRight; + } + + if (isLayoutRtl) { + final int temp = indicatorRect.left; + indicatorRect.left = width - indicatorRect.right; + indicatorRect.right = width - temp; + + indicatorRect.left -= getPaddingRight(); + indicatorRect.right -= getPaddingRight(); + } else { + indicatorRect.left += getPaddingLeft(); + indicatorRect.right += getPaddingLeft(); + } + + lastItemType = pos.position.type; + } + + if (indicatorRect.left != indicatorRect.right) { + // Use item's full height + the divider height + if (mStackFromBottom) { + // See ListView#dispatchDraw + indicatorRect.top = t; // - mDividerHeight; + indicatorRect.bottom = b; + } else { + indicatorRect.top = t; + indicatorRect.bottom = b; // + mDividerHeight; + } + + // Get the indicator (with its state set to the item's state) + indicator = getIndicator(pos); + if (indicator != null) { + // Draw the indicator + indicator.setBounds(indicatorRect); + indicator.draw(canvas); + } + } + pos.recycle(); + } + + if (clipToPadding) { + canvas.restoreToCount(saveCount); + } + } + + /** + * Gets the indicator for the item at the given position. If the indicator + * is stateful, the state will be given to the indicator. + * + * @param pos The flat list position of the item whose indicator + * should be returned. + * @return The indicator in the proper state. + */ + private Drawable getIndicator(PositionMetadata pos) { + Drawable indicator; + + if (pos.position.type == ExpandableListPosition.GROUP) { + indicator = mGroupIndicator; + + if (indicator != null && indicator.isStateful()) { + // Empty check based on availability of data. If the groupMetadata isn't null, + // we do a check on it. Otherwise, the group is collapsed so we consider it + // empty for performance reasons. + boolean isEmpty = (pos.groupMetadata == null) || + (pos.groupMetadata.lastChildFlPos == pos.groupMetadata.flPos); + + final int stateSetIndex = + (pos.isExpanded() ? 1 : 0) | // Expanded? + (isEmpty ? 2 : 0); // Empty? + indicator.setState(GROUP_STATE_SETS[stateSetIndex]); + } + } else { + indicator = mChildIndicator; + + if (indicator != null && indicator.isStateful()) { + // No need for a state sets array for the child since it only has two states + final int stateSet[] = pos.position.flatListPos == pos.groupMetadata.lastChildFlPos + ? CHILD_LAST_STATE_SET + : EMPTY_STATE_SET; + indicator.setState(stateSet); + } + } + + return indicator; + } + + /** + * Sets the drawable that will be drawn adjacent to every child in the list. This will + * be drawn using the same height as the normal divider ({@link #setDivider(Drawable)}) or + * if it does not have an intrinsic height, the height set by {@link #setDividerHeight(int)}. + * + * @param childDivider The drawable to use. + */ + public void setChildDivider(Drawable childDivider) { + mChildDivider = childDivider; + } + + @Override + void drawDivider(Canvas canvas, Rect bounds, int childIndex) { + int flatListPosition = childIndex + mFirstPosition; + + // Only proceed as possible child if the divider isn't above all items (if it is above + // all items, then the item below it has to be a group) + if (flatListPosition >= 0) { + final int adjustedPosition = getFlatPositionForConnector(flatListPosition); + PositionMetadata pos = mConnector.getUnflattenedPos(adjustedPosition); + // If this item is a child, or it is a non-empty group that is expanded + if ((pos.position.type == ExpandableListPosition.CHILD) || (pos.isExpanded() && + pos.groupMetadata.lastChildFlPos != pos.groupMetadata.flPos)) { + // These are the cases where we draw the child divider + final Drawable divider = mChildDivider; + divider.setBounds(bounds); + divider.draw(canvas); + pos.recycle(); + return; + } + pos.recycle(); + } + + // Otherwise draw the default divider + super.drawDivider(canvas, bounds, flatListPosition); + } + + /** + * This overloaded method should not be used, instead use + * {@link #setAdapter(ExpandableListAdapter)}. + *

    + * {@inheritDoc} + */ + @Override + public void setAdapter(ListAdapter adapter) { + throw new RuntimeException( + "For ExpandableListView, use setAdapter(ExpandableListAdapter) instead of " + + + "setAdapter(ListAdapter)"); + } + + /** + * This method should not be used, use {@link #getExpandableListAdapter()}. + */ + @Override + public ListAdapter getAdapter() { + /* + * The developer should never really call this method on an + * ExpandableListView, so it would be nice to throw a RuntimeException, + * but AdapterView calls this + */ + return super.getAdapter(); + } + + /** + * Register a callback to be invoked when an item has been clicked and the + * caller prefers to receive a ListView-style position instead of a group + * and/or child position. In most cases, the caller should use + * {@link #setOnGroupClickListener} and/or {@link #setOnChildClickListener}. + *

    + * {@inheritDoc} + */ + @Override + public void setOnItemClickListener(OnItemClickListener l) { + super.setOnItemClickListener(l); + } + + /** + * Sets the adapter that provides data to this view. + * @param adapter The adapter that provides data to this view. + */ + public void setAdapter(ExpandableListAdapter adapter) { + // Set member variable + mAdapter = adapter; + + if (adapter != null) { + // Create the connector + mConnector = new ExpandableListConnector(adapter); + } else { + mConnector = null; + } + + // Link the ListView (superclass) to the expandable list data through the connector + super.setAdapter(mConnector); + } + + /** + * Gets the adapter that provides data to this view. + * @return The adapter that provides data to this view. + */ + public ExpandableListAdapter getExpandableListAdapter() { + return mAdapter; + } + + /** + * @param position An absolute (including header and footer) flat list position. + * @return true if the position corresponds to a header or a footer item. + */ + private boolean isHeaderOrFooterPosition(int position) { + final int footerViewsStart = mItemCount - getFooterViewsCount(); + return (position < getHeaderViewsCount() || position >= footerViewsStart); + } + + /** + * Converts an absolute item flat position into a group/child flat position, shifting according + * to the number of header items. + * + * @param flatListPosition The absolute flat position + * @return A group/child flat position as expected by the connector. + */ + private int getFlatPositionForConnector(int flatListPosition) { + return flatListPosition - getHeaderViewsCount(); + } + + /** + * Converts a group/child flat position into an absolute flat position, that takes into account + * the possible headers. + * + * @param flatListPosition The child/group flat position + * @return An absolute flat position. + */ + private int getAbsoluteFlatPosition(int flatListPosition) { + return flatListPosition + getHeaderViewsCount(); + } + + @Override + public boolean performItemClick(View v, int position, long id) { + // Ignore clicks in header/footers + if (isHeaderOrFooterPosition(position)) { + // Clicked on a header/footer, so ignore pass it on to super + return super.performItemClick(v, position, id); + } + + // Internally handle the item click + final int adjustedPosition = getFlatPositionForConnector(position); + final boolean clicked = handleItemClick(v, adjustedPosition, id); + if (v != null) { + //v.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); + } + return clicked; + } + + /** + * This will either expand/collapse groups (if a group was clicked) or pass + * on the click to the proper child (if a child was clicked) + * + * @param position The flat list position. This has already been factored to + * remove the header/footer. + * @param id The ListAdapter ID, not the group or child ID. + */ + boolean handleItemClick(View v, int position, long id) { + final PositionMetadata posMetadata = mConnector.getUnflattenedPos(position); + + id = getChildOrGroupId(posMetadata.position); + + boolean returnValue; + if (posMetadata.position.type == ExpandableListPosition.GROUP) { + /* It's a group, so handle collapsing/expanding */ + + /* It's a group click, so pass on event */ + if (mOnGroupClickListener != null) { + if (mOnGroupClickListener.onGroupClick(this, v, + posMetadata.position.groupPos, id)) { + posMetadata.recycle(); + return true; + } + } + + if (posMetadata.isExpanded()) { + /* Collapse it */ + mConnector.collapseGroup(posMetadata); + + playSoundEffect(SoundEffectConstants.CLICK); + + if (mOnGroupCollapseListener != null) { + mOnGroupCollapseListener.onGroupCollapse(posMetadata.position.groupPos); + } + } else { + /* Expand it */ + mConnector.expandGroup(posMetadata); + + playSoundEffect(SoundEffectConstants.CLICK); + + if (mOnGroupExpandListener != null) { + mOnGroupExpandListener.onGroupExpand(posMetadata.position.groupPos); + } + + final int groupPos = posMetadata.position.groupPos; + final int groupFlatPos = posMetadata.position.flatListPos; + + final int shiftedGroupPosition = groupFlatPos + getHeaderViewsCount(); + smoothScrollToPosition(shiftedGroupPosition + mAdapter.getChildrenCount(groupPos), + shiftedGroupPosition); + } + + returnValue = true; + } else { + /* It's a child, so pass on event */ + if (mOnChildClickListener != null) { + playSoundEffect(SoundEffectConstants.CLICK); + return mOnChildClickListener.onChildClick(this, v, posMetadata.position.groupPos, + posMetadata.position.childPos, id); + } + + returnValue = false; + } + + posMetadata.recycle(); + + return returnValue; + } + + /** + * Expand a group in the grouped list view + * + * @param groupPos the group to be expanded + * @return True if the group was expanded, false otherwise (if the group + * was already expanded, this will return false) + */ + public boolean expandGroup(int groupPos) { + return expandGroup(groupPos, false); + } + + /** + * Expand a group in the grouped list view + * + * @param groupPos the group to be expanded + * @param animate true if the expanding group should be animated in + * @return True if the group was expanded, false otherwise (if the group + * was already expanded, this will return false) + */ + public boolean expandGroup(int groupPos, boolean animate) { + ExpandableListPosition elGroupPos = ExpandableListPosition.obtain( + ExpandableListPosition.GROUP, groupPos, -1, -1); + PositionMetadata pm = mConnector.getFlattenedPos(elGroupPos); + elGroupPos.recycle(); + boolean retValue = mConnector.expandGroup(pm); + + if (mOnGroupExpandListener != null) { + mOnGroupExpandListener.onGroupExpand(groupPos); + } + + if (animate) { + final int groupFlatPos = pm.position.flatListPos; + + final int shiftedGroupPosition = groupFlatPos + getHeaderViewsCount(); + smoothScrollToPosition(shiftedGroupPosition + mAdapter.getChildrenCount(groupPos), + shiftedGroupPosition); + } + pm.recycle(); + + return retValue; + } + + /** + * Collapse a group in the grouped list view + * + * @param groupPos position of the group to collapse + * @return True if the group was collapsed, false otherwise (if the group + * was already collapsed, this will return false) + */ + public boolean collapseGroup(int groupPos) { + boolean retValue = mConnector.collapseGroup(groupPos); + + if (mOnGroupCollapseListener != null) { + mOnGroupCollapseListener.onGroupCollapse(groupPos); + } + + return retValue; + } + + /** + * Used for being notified when a group is collapsed + */ + public interface OnGroupCollapseListener { + /** + * Callback method to be invoked when a group in this expandable list has + * been collapsed. + * + * @param groupPosition The group position that was collapsed + */ + void onGroupCollapse(int groupPosition); + } + + //@UnsupportedAppUsage + private OnGroupCollapseListener mOnGroupCollapseListener; + + public void setOnGroupCollapseListener( + OnGroupCollapseListener onGroupCollapseListener) { + mOnGroupCollapseListener = onGroupCollapseListener; + } + + /** + * Used for being notified when a group is expanded + */ + public interface OnGroupExpandListener { + /** + * Callback method to be invoked when a group in this expandable list has + * been expanded. + * + * @param groupPosition The group position that was expanded + */ + void onGroupExpand(int groupPosition); + } + + //@UnsupportedAppUsage + private OnGroupExpandListener mOnGroupExpandListener; + + public void setOnGroupExpandListener( + OnGroupExpandListener onGroupExpandListener) { + mOnGroupExpandListener = onGroupExpandListener; + } + + /** + * Interface definition for a callback to be invoked when a group in this + * expandable list has been clicked. + */ + public interface OnGroupClickListener { + /** + * Callback method to be invoked when a group in this expandable list has + * been clicked. + * + * @param parent The ExpandableListConnector where the click happened + * @param v The view within the expandable list/ListView that was clicked + * @param groupPosition The group position that was clicked + * @param id The row id of the group that was clicked + * @return True if the click was handled + */ + boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, + long id); + } + + //@UnsupportedAppUsage + private OnGroupClickListener mOnGroupClickListener; + + public void setOnGroupClickListener(OnGroupClickListener onGroupClickListener) { + mOnGroupClickListener = onGroupClickListener; + } + + /** + * Interface definition for a callback to be invoked when a child in this + * expandable list has been clicked. + */ + public interface OnChildClickListener { + /** + * Callback method to be invoked when a child in this expandable list has + * been clicked. + * + * @param parent The ExpandableListView where the click happened + * @param v The view within the expandable list/ListView that was clicked + * @param groupPosition The group position that contains the child that + * was clicked + * @param childPosition The child position within the group + * @param id The row id of the child that was clicked + * @return True if the click was handled + */ + boolean onChildClick(ExpandableListView parent, View v, int groupPosition, + int childPosition, long id); + } + + //@UnsupportedAppUsage + private OnChildClickListener mOnChildClickListener; + + public void setOnChildClickListener(OnChildClickListener onChildClickListener) { + mOnChildClickListener = onChildClickListener; + } + + /** + * Converts a flat list position (the raw position of an item (child or group) + * in the list) to a group and/or child position (represented in a + * packed position). This is useful in situations where the caller needs to + * use the underlying {@link ListView}'s methods. Use + * {@link ExpandableListView#getPackedPositionType} , + * {@link ExpandableListView#getPackedPositionChild}, + * {@link ExpandableListView#getPackedPositionGroup} to unpack. + * + * @param flatListPosition The flat list position to be converted. + * @return The group and/or child position for the given flat list position + * in packed position representation. #PACKED_POSITION_VALUE_NULL if + * the position corresponds to a header or a footer item. + */ + public long getExpandableListPosition(int flatListPosition) { + if (isHeaderOrFooterPosition(flatListPosition)) { + return PACKED_POSITION_VALUE_NULL; + } + + final int adjustedPosition = getFlatPositionForConnector(flatListPosition); + PositionMetadata pm = mConnector.getUnflattenedPos(adjustedPosition); + long packedPos = pm.position.getPackedPosition(); + pm.recycle(); + return packedPos; + } + + /** + * Converts a group and/or child position to a flat list position. This is + * useful in situations where the caller needs to use the underlying + * {@link ListView}'s methods. + * + * @param packedPosition The group and/or child positions to be converted in + * packed position representation. Use + * {@link #getPackedPositionForChild(int, int)} or + * {@link #getPackedPositionForGroup(int)}. + * @return The flat list position for the given child or group. + */ + public int getFlatListPosition(long packedPosition) { + ExpandableListPosition elPackedPos = ExpandableListPosition + .obtainPosition(packedPosition); + PositionMetadata pm = mConnector.getFlattenedPos(elPackedPos); + elPackedPos.recycle(); + final int flatListPosition = pm.position.flatListPos; + pm.recycle(); + return getAbsoluteFlatPosition(flatListPosition); + } + + /** + * Gets the position of the currently selected group or child (along with + * its type). Can return {@link #PACKED_POSITION_VALUE_NULL} if no selection. + * + * @return A packed position containing the currently selected group or + * child's position and type. #PACKED_POSITION_VALUE_NULL if no selection + * or if selection is on a header or a footer item. + */ + public long getSelectedPosition() { + final int selectedPos = getSelectedItemPosition(); + + // The case where there is no selection (selectedPos == -1) is also handled here. + return getExpandableListPosition(selectedPos); + } + + /** + * Gets the ID of the currently selected group or child. Can return -1 if no + * selection. + * + * @return The ID of the currently selected group or child. -1 if no + * selection. + */ + public long getSelectedId() { + long packedPos = getSelectedPosition(); + if (packedPos == PACKED_POSITION_VALUE_NULL) + return -1; + + int groupPos = getPackedPositionGroup(packedPos); + + if (getPackedPositionType(packedPos) == PACKED_POSITION_TYPE_GROUP) { + // It's a group + return mAdapter.getGroupId(groupPos); + } else { + // It's a child + return mAdapter.getChildId(groupPos, getPackedPositionChild(packedPos)); + } + } + + /** + * Sets the selection to the specified group. + * @param groupPosition The position of the group that should be selected. + */ + public void setSelectedGroup(int groupPosition) { + ExpandableListPosition elGroupPos = ExpandableListPosition + .obtainGroupPosition(groupPosition); + PositionMetadata pm = mConnector.getFlattenedPos(elGroupPos); + elGroupPos.recycle(); + final int absoluteFlatPosition = getAbsoluteFlatPosition(pm.position.flatListPos); + super.setSelection(absoluteFlatPosition); + pm.recycle(); + } + + /** + * Sets the selection to the specified child. If the child is in a collapsed + * group, the group will only be expanded and child subsequently selected if + * shouldExpandGroup is set to true, otherwise the method will return false. + * + * @param groupPosition The position of the group that contains the child. + * @param childPosition The position of the child within the group. + * @param shouldExpandGroup Whether the child's group should be expanded if + * it is collapsed. + * @return Whether the selection was successfully set on the child. + */ + public boolean setSelectedChild(int groupPosition, int childPosition, boolean shouldExpandGroup) { + ExpandableListPosition elChildPos = ExpandableListPosition.obtainChildPosition( + groupPosition, childPosition); + PositionMetadata flatChildPos = mConnector.getFlattenedPos(elChildPos); + + if (flatChildPos == null) { + // The child's group isn't expanded + + // Shouldn't expand the group, so return false for we didn't set the selection + if (!shouldExpandGroup) + return false; + + expandGroup(groupPosition); + + flatChildPos = mConnector.getFlattenedPos(elChildPos); + + // Validity check + if (flatChildPos == null) { + throw new IllegalStateException("Could not find child"); + } + } + + int absoluteFlatPosition = getAbsoluteFlatPosition(flatChildPos.position.flatListPos); + super.setSelection(absoluteFlatPosition); + + elChildPos.recycle(); + flatChildPos.recycle(); + + 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) { + return mConnector.isGroupExpanded(groupPosition); + } + + /** + * Gets the type of a packed position. See + * {@link #getPackedPositionForChild(int, int)}. + * + * @param packedPosition The packed position for which to return the type. + * @return The type of the position contained within the packed position, + * either {@link #PACKED_POSITION_TYPE_CHILD}, {@link #PACKED_POSITION_TYPE_GROUP}, or + * {@link #PACKED_POSITION_TYPE_NULL}. + */ + public static int getPackedPositionType(long packedPosition) { + if (packedPosition == PACKED_POSITION_VALUE_NULL) { + return PACKED_POSITION_TYPE_NULL; + } + + return (packedPosition & PACKED_POSITION_MASK_TYPE) == PACKED_POSITION_MASK_TYPE + ? PACKED_POSITION_TYPE_CHILD + : PACKED_POSITION_TYPE_GROUP; + } + + /** + * Gets the group position from a packed position. See + * {@link #getPackedPositionForChild(int, int)}. + * + * @param packedPosition The packed position from which the group position + * will be returned. + * @return The group position portion of the packed position. If this does + * not contain a group, returns -1. + */ public static int getPackedPositionGroup(long packedPosition) { - return 0; + // Null + if (packedPosition == PACKED_POSITION_VALUE_NULL) + return -1; + + return (int)((packedPosition & PACKED_POSITION_MASK_GROUP) >> PACKED_POSITION_SHIFT_GROUP); } - public void setHeaderDividersEnabled(boolean enabled) {} + /** + * Gets the child position from a packed position that is of + * {@link #PACKED_POSITION_TYPE_CHILD} type (use {@link #getPackedPositionType(long)}). + * To get the group that this child belongs to, use + * {@link #getPackedPositionGroup(long)}. See + * {@link #getPackedPositionForChild(int, int)}. + * + * @param packedPosition The packed position from which the child position + * will be returned. + * @return The child position portion of the packed position. If this does + * not contain a child, returns -1. + */ + public static int getPackedPositionChild(long packedPosition) { + // Null + if (packedPosition == PACKED_POSITION_VALUE_NULL) + return -1; - public static interface OnChildClickListener { - abstract boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id); + // Group since a group type clears this bit + if ((packedPosition & PACKED_POSITION_MASK_TYPE) != PACKED_POSITION_MASK_TYPE) + return -1; + + return (int)(packedPosition & PACKED_POSITION_MASK_CHILD); + } + + /** + * Returns the packed position representation of a child's position. + *

    + * In general, a packed position should be used in + * situations where the position given to/returned from an + * {@link ExpandableListAdapter} or {@link ExpandableListView} method can + * either be a child or group. The two positions are packed into a single + * long which can be unpacked using + * {@link #getPackedPositionChild(long)}, + * {@link #getPackedPositionGroup(long)}, and + * {@link #getPackedPositionType(long)}. + * + * @param groupPosition The child's parent group's position. + * @param childPosition The child position within the group. + * @return The packed position representation of the child (and parent group). + */ + public static long getPackedPositionForChild(int groupPosition, int childPosition) { + return (((long)PACKED_POSITION_TYPE_CHILD) << PACKED_POSITION_SHIFT_TYPE) | ((((long)groupPosition) & PACKED_POSITION_INT_MASK_GROUP) << PACKED_POSITION_SHIFT_GROUP) | (childPosition & PACKED_POSITION_INT_MASK_CHILD); + } + + /** + * Returns the packed position representation of a group's position. See + * {@link #getPackedPositionForChild(int, int)}. + * + * @param groupPosition The child's parent group's position. + * @return The packed position representation of the group. + */ + public static long getPackedPositionForGroup(int groupPosition) { + // No need to OR a type in because PACKED_POSITION_GROUP == 0 + return ((((long)groupPosition) & PACKED_POSITION_INT_MASK_GROUP) + << PACKED_POSITION_SHIFT_GROUP); + } + + /*@Override + ContextMenuInfo createContextMenuInfo(View view, int flatListPosition, long id) { + if (isHeaderOrFooterPosition(flatListPosition)) { + // Return normal info for header/footer view context menus + return new AdapterContextMenuInfo(view, flatListPosition, id); + } + + final int adjustedPosition = getFlatPositionForConnector(flatListPosition); + PositionMetadata pm = mConnector.getUnflattenedPos(adjustedPosition); + ExpandableListPosition pos = pm.position; + + id = getChildOrGroupId(pos); + long packedPosition = pos.getPackedPosition(); + + pm.recycle(); + + return new ExpandableListContextMenuInfo(view, packedPosition, id); + }*/ + + /** + * @hide + */ + /*@Override + public void onInitializeAccessibilityNodeInfoForItem( + View view, int position, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoForItem(view, position, info); + final PositionMetadata metadata = mConnector.getUnflattenedPos(position); + if (metadata.position.type == ExpandableListPosition.GROUP) { + if (view != null && view.isEnabled()) { + info.setClickable(true); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK); + if (isGroupExpanded(metadata.position.groupPos)) { + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE); + } else { + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); + } + } + } + + metadata.recycle(); + }*/ + + /** + * Gets the ID of the group or child at the given position. + * This is useful since there is no ListAdapter ID -> ExpandableListAdapter + * ID conversion mechanism (in some cases, it isn't possible). + * + * @param position The position of the child or group whose ID should be + * returned. + */ + private long getChildOrGroupId(ExpandableListPosition position) { + if (position.type == ExpandableListPosition.CHILD) { + return mAdapter.getChildId(position.groupPos, position.childPos); + } else { + return mAdapter.getGroupId(position.groupPos); + } + } + + /** + * Sets the indicator to be drawn next to a child. + * + * @param childIndicator The drawable to be used as an indicator. If the + * child is the last child for a group, the state + * {@link android.R.attr#state_last} will be set. + */ + public void setChildIndicator(Drawable childIndicator) { + mChildIndicator = childIndicator; + } + + /** + * Sets the drawing bounds for the child indicator. For either, you can + * specify {@link #CHILD_INDICATOR_INHERIT} to use inherit from the general + * indicator's bounds. + * + * @see #setIndicatorBounds(int, int) + * @param left The left position (relative to the left bounds of this View) + * to start drawing the indicator. + * @param right The right position (relative to the left bounds of this + * View) to end the drawing of the indicator. + */ + public void setChildIndicatorBounds(int left, int right) { + mChildIndicatorLeft = left; + mChildIndicatorRight = right; + resolveChildIndicator(); + } + + /** + * Sets the relative drawing bounds for the child indicator. For either, you can + * specify {@link #CHILD_INDICATOR_INHERIT} to use inherit from the general + * indicator's bounds. + * + * @see #setIndicatorBounds(int, int) + * @param start The start position (relative to the start bounds of this View) + * to start drawing the indicator. + * @param end The end position (relative to the end bounds of this + * View) to end the drawing of the indicator. + */ + public void setChildIndicatorBoundsRelative(int start, int end) { + mChildIndicatorStart = start; + mChildIndicatorEnd = end; + resolveChildIndicator(); + } + + /** + * Sets the indicator to be drawn next to a group. + * + * @param groupIndicator The drawable to be used as an indicator. If the + * group is empty, the state {@link android.R.attr#state_empty} will be + * set. If the group is expanded, the state + * {@link android.R.attr#state_expanded} will be set. + */ + public void setGroupIndicator(Drawable groupIndicator) { + mGroupIndicator = groupIndicator; + if (mIndicatorRight == 0 && mGroupIndicator != null) { + mIndicatorRight = mIndicatorLeft + mGroupIndicator.getIntrinsicWidth(); + } + } + + /** + * Sets the drawing bounds for the indicators (at minimum, the group indicator + * is affected by this; the child indicator is affected by this if the + * child indicator bounds are set to inherit). + * + * @see #setChildIndicatorBounds(int, int) + * @param left The left position (relative to the left bounds of this View) + * to start drawing the indicator. + * @param right The right position (relative to the left bounds of this + * View) to end the drawing of the indicator. + */ + public void setIndicatorBounds(int left, int right) { + mIndicatorLeft = left; + mIndicatorRight = right; + resolveIndicator(); + } + + /** + * Sets the relative drawing bounds for the indicators (at minimum, the group indicator + * is affected by this; the child indicator is affected by this if the + * child indicator bounds are set to inherit). + * + * @see #setChildIndicatorBounds(int, int) + * @param start The start position (relative to the start bounds of this View) + * to start drawing the indicator. + * @param end The end position (relative to the end bounds of this + * View) to end the drawing of the indicator. + */ + public void setIndicatorBoundsRelative(int start, int end) { + mIndicatorStart = start; + mIndicatorEnd = end; + resolveIndicator(); + } + + /** + * Extra menu information specific to an {@link ExpandableListView} provided + * to the + * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) } + * callback when a context menu is brought up for this AdapterView. + */ + /*public static class ExpandableListContextMenuInfo implements ContextMenu.ContextMenuInfo { + + public ExpandableListContextMenuInfo(View targetView, long packedPosition, long id) { + this.targetView = targetView; + this.packedPosition = packedPosition; + this.id = id; + } + + /** + * The view for which the context menu is being displayed. This + * will be one of the children Views of this {@link ExpandableListView}. + * / + public View targetView; + + /** + * The packed position in the list represented by the adapter for which + * the context menu is being displayed. Use the methods + * {@link ExpandableListView#getPackedPositionType}, + * {@link ExpandableListView#getPackedPositionChild}, and + * {@link ExpandableListView#getPackedPositionGroup} to unpack this. + * / + public long packedPosition; + + /** + * The ID of the item (group or child) for which the context menu is + * being displayed. + * / + public long id; + }*/ + + /*static class SavedState extends BaseSavedState { + ArrayList expandedGroupMetadataList; + + /** + * Constructor called from {@link ExpandableListView#onSaveInstanceState()} + * / + SavedState( + Parcelable superState, + ArrayList expandedGroupMetadataList) { + super(superState); + this.expandedGroupMetadataList = expandedGroupMetadataList; + } + + /** + * Constructor called from {@link #CREATOR} + * / + private SavedState(Parcel in) { + super(in); + expandedGroupMetadataList = new ArrayList(); + in.readList(expandedGroupMetadataList, ExpandableListConnector.class.getClassLoader(), android.widget.ExpandableListConnector.GroupMetadata.class); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeList(expandedGroupMetadataList); + } + + public static final @android.annotation.NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + return new SavedState(superState, + mConnector != null ? mConnector.getExpandedGroupMetadataList() : null); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + SavedState ss = (SavedState)state; + super.onRestoreInstanceState(ss.getSuperState()); + + if (mConnector != null && ss.expandedGroupMetadataList != null) { + mConnector.setExpandedGroupMetadataList(ss.expandedGroupMetadataList); + } + }*/ + + //@Override + public CharSequence getAccessibilityClassName() { + return ExpandableListView.class.getName(); } } diff --git a/src/api-impl/android/widget/HeterogeneousExpandableList.java b/src/api-impl/android/widget/HeterogeneousExpandableList.java new file mode 100644 index 00000000..47d17e22 --- /dev/null +++ b/src/api-impl/android/widget/HeterogeneousExpandableList.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2006 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.view.View; +import android.view.ViewGroup; + +/** + * Additional methods that when implemented make an + * {@link ExpandableListAdapter} take advantage of the {@link Adapter} view type + * mechanism. + *

    + * An {@link ExpandableListAdapter} declares it has one view type for its group items + * and one view type for its child items. Although adapted for most {@link ExpandableListView}s, + * these values should be tuned for heterogeneous {@link ExpandableListView}s. + *

    + * Lists that contain different types of group and/or child item views, should use an adapter that + * implements this interface. This way, the recycled views that will be provided to + * {@link android.widget.ExpandableListAdapter#getGroupView(int, boolean, View, ViewGroup)} + * and + * {@link android.widget.ExpandableListAdapter#getChildView(int, int, boolean, View, ViewGroup)} + * will be of the appropriate group or child type, resulting in a more efficient reuse of the + * previously created views. + */ +public interface HeterogeneousExpandableList { + /** + * Get the type of group View that will be created by + * {@link android.widget.ExpandableListAdapter#getGroupView(int, boolean, View, ViewGroup)} + * . for the specified group item. + * + * @param groupPosition the position of the group for which the type should be returned. + * @return An integer representing the type of group View. Two group views should share the same + * type if one can be converted to the other in + * {@link android.widget.ExpandableListAdapter#getGroupView(int, boolean, View, ViewGroup)} + * . Note: Integers must be in the range 0 to {@link #getGroupTypeCount} - 1. + * {@link android.widget.Adapter#IGNORE_ITEM_VIEW_TYPE} can also be returned. + * @see android.widget.Adapter#IGNORE_ITEM_VIEW_TYPE + * @see #getGroupTypeCount() + */ + int getGroupType(int groupPosition); + + /** + * Get the type of child View that will be created by + * {@link android.widget.ExpandableListAdapter#getChildView(int, int, boolean, View, ViewGroup)} + * for the specified child item. + * + * @param groupPosition the position of the group that the child resides in + * @param childPosition the position of the child with respect to other children in the group + * @return An integer representing the type of child View. Two child views should share the same + * type if one can be converted to the other in + * {@link android.widget.ExpandableListAdapter#getChildView(int, int, boolean, View, ViewGroup)} + * Note: Integers must be in the range 0 to {@link #getChildTypeCount} - 1. + * {@link android.widget.Adapter#IGNORE_ITEM_VIEW_TYPE} can also be returned. + * @see android.widget.Adapter#IGNORE_ITEM_VIEW_TYPE + * @see #getChildTypeCount() + */ + int getChildType(int groupPosition, int childPosition); + + /** + *

    + * Returns the number of types of group Views that will be created by + * {@link android.widget.ExpandableListAdapter#getGroupView(int, boolean, View, ViewGroup)} + * . Each type represents a set of views that can be converted in + * {@link android.widget.ExpandableListAdapter#getGroupView(int, boolean, View, ViewGroup)} + * . If the adapter always returns the same type of View for all group items, this method should + * return 1. + *

    + * This method will only be called when the adapter is set on the {@link AdapterView}. + * + * @return The number of types of group Views that will be created by this adapter. + * @see #getChildTypeCount() + * @see #getGroupType(int) + */ + int getGroupTypeCount(); + + /** + *

    + * Returns the number of types of child Views that will be created by + * {@link android.widget.ExpandableListAdapter#getChildView(int, int, boolean, View, ViewGroup)} + * . Each type represents a set of views that can be converted in + * {@link android.widget.ExpandableListAdapter#getChildView(int, int, boolean, View, ViewGroup)} + * , for any group. If the adapter always returns the same type of View for + * all child items, this method should return 1. + *

    + * This method will only be called when the adapter is set on the {@link AdapterView}. + * + * @return The total number of types of child Views that will be created by this adapter. + * @see #getGroupTypeCount() + * @see #getChildType(int, int) + */ + int getChildTypeCount(); +} diff --git a/src/api-impl/meson.build b/src/api-impl/meson.build index 2f8a76f7..2afa31db 100644 --- a/src/api-impl/meson.build +++ b/src/api-impl/meson.build @@ -549,6 +549,7 @@ srcs = [ 'android/widget/ArrayAdapter.java', 'android/widget/AutoCompleteTextView.java', 'android/widget/BaseAdapter.java', + 'android/widget/BaseExpandableListAdapter.java', 'android/widget/Button.java', 'android/widget/CheckBox.java', 'android/widget/Checkable.java', @@ -557,6 +558,9 @@ srcs = [ 'android/widget/CursorAdapter.java', 'android/widget/EdgeEffect.java', 'android/widget/EditText.java', + 'android/widget/ExpandableListAdapter.java', + 'android/widget/ExpandableListConnector.java', + 'android/widget/ExpandableListPosition.java', 'android/widget/ExpandableListView.java', 'android/widget/Filter.java', 'android/widget/Filterable.java', @@ -565,6 +569,7 @@ srcs = [ 'android/widget/Gallery.java', 'android/widget/GridView.java', 'android/widget/HeaderViewListAdapter.java', + 'android/widget/HeterogeneousExpandableList.java', 'android/widget/HorizontalScrollView.java', 'android/widget/ImageButton.java', 'android/widget/ImageView.java',