Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2006 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.widget;
     18 
     19 import com.android.internal.R;
     20 
     21 import android.content.Context;
     22 import android.content.res.TypedArray;
     23 import android.graphics.Canvas;
     24 import android.graphics.Rect;
     25 import android.graphics.drawable.Drawable;
     26 import android.os.Parcel;
     27 import android.os.Parcelable;
     28 import android.util.AttributeSet;
     29 import android.view.ContextMenu;
     30 import android.view.SoundEffectConstants;
     31 import android.view.View;
     32 import android.view.ContextMenu.ContextMenuInfo;
     33 import android.view.accessibility.AccessibilityEvent;
     34 import android.view.accessibility.AccessibilityNodeInfo;
     35 import android.widget.ExpandableListConnector.PositionMetadata;
     36 
     37 import java.util.ArrayList;
     38 
     39 /**
     40  * A view that shows items in a vertically scrolling two-level list. This
     41  * differs from the {@link ListView} by allowing two levels: groups which can
     42  * individually be expanded to show its children. The items come from the
     43  * {@link ExpandableListAdapter} associated with this view.
     44  * <p>
     45  * Expandable lists are able to show an indicator beside each item to display
     46  * the item's current state (the states are usually one of expanded group,
     47  * collapsed group, child, or last child). Use
     48  * {@link #setChildIndicator(Drawable)} or {@link #setGroupIndicator(Drawable)}
     49  * (or the corresponding XML attributes) to set these indicators (see the docs
     50  * for each method to see additional state that each Drawable can have). The
     51  * default style for an {@link ExpandableListView} provides indicators which
     52  * will be shown next to Views given to the {@link ExpandableListView}. The
     53  * layouts android.R.layout.simple_expandable_list_item_1 and
     54  * android.R.layout.simple_expandable_list_item_2 (which should be used with
     55  * {@link SimpleCursorTreeAdapter}) contain the preferred position information
     56  * for indicators.
     57  * <p>
     58  * The context menu information set by an {@link ExpandableListView} will be a
     59  * {@link ExpandableListContextMenuInfo} object with
     60  * {@link ExpandableListContextMenuInfo#packedPosition} being a packed position
     61  * that can be used with {@link #getPackedPositionType(long)} and the other
     62  * similar methods.
     63  * <p>
     64  * <em><b>Note:</b></em> You cannot use the value <code>wrap_content</code>
     65  * for the <code>android:layout_height</code> attribute of a
     66  * ExpandableListView in XML if the parent's size is also not strictly specified
     67  * (for example, if the parent were ScrollView you could not specify
     68  * wrap_content since it also can be any length. However, you can use
     69  * wrap_content if the ExpandableListView parent has a specific size, such as
     70  * 100 pixels.
     71  *
     72  * @attr ref android.R.styleable#ExpandableListView_groupIndicator
     73  * @attr ref android.R.styleable#ExpandableListView_indicatorLeft
     74  * @attr ref android.R.styleable#ExpandableListView_indicatorRight
     75  * @attr ref android.R.styleable#ExpandableListView_childIndicator
     76  * @attr ref android.R.styleable#ExpandableListView_childIndicatorLeft
     77  * @attr ref android.R.styleable#ExpandableListView_childIndicatorRight
     78  * @attr ref android.R.styleable#ExpandableListView_childDivider
     79  */
     80 public class ExpandableListView extends ListView {
     81 
     82     /**
     83      * The packed position represents a group.
     84      */
     85     public static final int PACKED_POSITION_TYPE_GROUP = 0;
     86 
     87     /**
     88      * The packed position represents a child.
     89      */
     90     public static final int PACKED_POSITION_TYPE_CHILD = 1;
     91 
     92     /**
     93      * The packed position represents a neither/null/no preference.
     94      */
     95     public static final int PACKED_POSITION_TYPE_NULL = 2;
     96 
     97     /**
     98      * The value for a packed position that represents neither/null/no
     99      * preference. This value is not otherwise possible since a group type
    100      * (first bit 0) should not have a child position filled.
    101      */
    102     public static final long PACKED_POSITION_VALUE_NULL = 0x00000000FFFFFFFFL;
    103 
    104     /** The mask (in packed position representation) for the child */
    105     private static final long PACKED_POSITION_MASK_CHILD = 0x00000000FFFFFFFFL;
    106 
    107     /** The mask (in packed position representation) for the group */
    108     private static final long PACKED_POSITION_MASK_GROUP = 0x7FFFFFFF00000000L;
    109 
    110     /** The mask (in packed position representation) for the type */
    111     private static final long PACKED_POSITION_MASK_TYPE  = 0x8000000000000000L;
    112 
    113     /** The shift amount (in packed position representation) for the group */
    114     private static final long PACKED_POSITION_SHIFT_GROUP = 32;
    115 
    116     /** The shift amount (in packed position representation) for the type */
    117     private static final long PACKED_POSITION_SHIFT_TYPE  = 63;
    118 
    119     /** The mask (in integer child position representation) for the child */
    120     private static final long PACKED_POSITION_INT_MASK_CHILD = 0xFFFFFFFF;
    121 
    122     /** The mask (in integer group position representation) for the group */
    123     private static final long PACKED_POSITION_INT_MASK_GROUP = 0x7FFFFFFF;
    124 
    125     /** Serves as the glue/translator between a ListView and an ExpandableListView */
    126     private ExpandableListConnector mConnector;
    127 
    128     /** Gives us Views through group+child positions */
    129     private ExpandableListAdapter mAdapter;
    130 
    131     /** Left bound for drawing the indicator. */
    132     private int mIndicatorLeft;
    133 
    134     /** Right bound for drawing the indicator. */
    135     private int mIndicatorRight;
    136 
    137     /**
    138      * Left bound for drawing the indicator of a child. Value of
    139      * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorLeft.
    140      */
    141     private int mChildIndicatorLeft;
    142 
    143     /**
    144      * Right bound for drawing the indicator of a child. Value of
    145      * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorRight.
    146      */
    147     private int mChildIndicatorRight;
    148 
    149     /**
    150      * Denotes when a child indicator should inherit this bound from the generic
    151      * indicator bounds
    152      */
    153     public static final int CHILD_INDICATOR_INHERIT = -1;
    154 
    155     /** The indicator drawn next to a group. */
    156     private Drawable mGroupIndicator;
    157 
    158     /** The indicator drawn next to a child. */
    159     private Drawable mChildIndicator;
    160 
    161     private static final int[] EMPTY_STATE_SET = {};
    162 
    163     /** State indicating the group is expanded. */
    164     private static final int[] GROUP_EXPANDED_STATE_SET =
    165             {R.attr.state_expanded};
    166 
    167     /** State indicating the group is empty (has no children). */
    168     private static final int[] GROUP_EMPTY_STATE_SET =
    169             {R.attr.state_empty};
    170 
    171     /** State indicating the group is expanded and empty (has no children). */
    172     private static final int[] GROUP_EXPANDED_EMPTY_STATE_SET =
    173             {R.attr.state_expanded, R.attr.state_empty};
    174 
    175     /** States for the group where the 0th bit is expanded and 1st bit is empty. */
    176     private static final int[][] GROUP_STATE_SETS = {
    177          EMPTY_STATE_SET, // 00
    178          GROUP_EXPANDED_STATE_SET, // 01
    179          GROUP_EMPTY_STATE_SET, // 10
    180          GROUP_EXPANDED_EMPTY_STATE_SET // 11
    181     };
    182 
    183     /** State indicating the child is the last within its group. */
    184     private static final int[] CHILD_LAST_STATE_SET =
    185             {R.attr.state_last};
    186 
    187     /** Drawable to be used as a divider when it is adjacent to any children */
    188     private Drawable mChildDivider;
    189 
    190     // Bounds of the indicator to be drawn
    191     private final Rect mIndicatorRect = new Rect();
    192 
    193     public ExpandableListView(Context context) {
    194         this(context, null);
    195     }
    196 
    197     public ExpandableListView(Context context, AttributeSet attrs) {
    198         this(context, attrs, com.android.internal.R.attr.expandableListViewStyle);
    199     }
    200 
    201     public ExpandableListView(Context context, AttributeSet attrs, int defStyle) {
    202         super(context, attrs, defStyle);
    203 
    204         TypedArray a =
    205             context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ExpandableListView, defStyle,
    206                     0);
    207 
    208         mGroupIndicator = a
    209                 .getDrawable(com.android.internal.R.styleable.ExpandableListView_groupIndicator);
    210         mChildIndicator = a
    211                 .getDrawable(com.android.internal.R.styleable.ExpandableListView_childIndicator);
    212         mIndicatorLeft = a
    213                 .getDimensionPixelSize(com.android.internal.R.styleable.ExpandableListView_indicatorLeft, 0);
    214         mIndicatorRight = a
    215                 .getDimensionPixelSize(com.android.internal.R.styleable.ExpandableListView_indicatorRight, 0);
    216         if (mIndicatorRight == 0 && mGroupIndicator != null) {
    217             mIndicatorRight = mIndicatorLeft + mGroupIndicator.getIntrinsicWidth();
    218         }
    219         mChildIndicatorLeft = a.getDimensionPixelSize(
    220                 com.android.internal.R.styleable.ExpandableListView_childIndicatorLeft, CHILD_INDICATOR_INHERIT);
    221         mChildIndicatorRight = a.getDimensionPixelSize(
    222                 com.android.internal.R.styleable.ExpandableListView_childIndicatorRight, CHILD_INDICATOR_INHERIT);
    223         mChildDivider = a.getDrawable(com.android.internal.R.styleable.ExpandableListView_childDivider);
    224 
    225         a.recycle();
    226     }
    227 
    228 
    229     @Override
    230     protected void dispatchDraw(Canvas canvas) {
    231         // Draw children, etc.
    232         super.dispatchDraw(canvas);
    233 
    234         // If we have any indicators to draw, we do it here
    235         if ((mChildIndicator == null) && (mGroupIndicator == null)) {
    236             return;
    237         }
    238 
    239         int saveCount = 0;
    240         final boolean clipToPadding = (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
    241         if (clipToPadding) {
    242             saveCount = canvas.save();
    243             final int scrollX = mScrollX;
    244             final int scrollY = mScrollY;
    245             canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
    246                     scrollX + mRight - mLeft - mPaddingRight,
    247                     scrollY + mBottom - mTop - mPaddingBottom);
    248         }
    249 
    250         final int headerViewsCount = getHeaderViewsCount();
    251 
    252         final int lastChildFlPos = mItemCount - getFooterViewsCount() - headerViewsCount - 1;
    253 
    254         final int myB = mBottom;
    255 
    256         PositionMetadata pos;
    257         View item;
    258         Drawable indicator;
    259         int t, b;
    260 
    261         // Start at a value that is neither child nor group
    262         int lastItemType = ~(ExpandableListPosition.CHILD | ExpandableListPosition.GROUP);
    263 
    264         final Rect indicatorRect = mIndicatorRect;
    265 
    266         // The "child" mentioned in the following two lines is this
    267         // View's child, not referring to an expandable list's
    268         // notion of a child (as opposed to a group)
    269         final int childCount = getChildCount();
    270         for (int i = 0, childFlPos = mFirstPosition - headerViewsCount; i < childCount;
    271              i++, childFlPos++) {
    272 
    273             if (childFlPos < 0) {
    274                 // This child is header
    275                 continue;
    276             } else if (childFlPos > lastChildFlPos) {
    277                 // This child is footer, so are all subsequent children
    278                 break;
    279             }
    280 
    281             item = getChildAt(i);
    282             t = item.getTop();
    283             b = item.getBottom();
    284 
    285             // This item isn't on the screen
    286             if ((b < 0) || (t > myB)) continue;
    287 
    288             // Get more expandable list-related info for this item
    289             pos = mConnector.getUnflattenedPos(childFlPos);
    290 
    291             // If this item type and the previous item type are different, then we need to change
    292             // the left & right bounds
    293             if (pos.position.type != lastItemType) {
    294                 if (pos.position.type == ExpandableListPosition.CHILD) {
    295                     indicatorRect.left = (mChildIndicatorLeft == CHILD_INDICATOR_INHERIT) ?
    296                             mIndicatorLeft : mChildIndicatorLeft;
    297                     indicatorRect.right = (mChildIndicatorRight == CHILD_INDICATOR_INHERIT) ?
    298                             mIndicatorRight : mChildIndicatorRight;
    299                 } else {
    300                     indicatorRect.left = mIndicatorLeft;
    301                     indicatorRect.right = mIndicatorRight;
    302                 }
    303 
    304                 indicatorRect.left += mPaddingLeft;
    305                 indicatorRect.right += mPaddingLeft;
    306 
    307                 lastItemType = pos.position.type;
    308             }
    309 
    310             if (indicatorRect.left != indicatorRect.right) {
    311                 // Use item's full height + the divider height
    312                 if (mStackFromBottom) {
    313                     // See ListView#dispatchDraw
    314                     indicatorRect.top = t;// - mDividerHeight;
    315                     indicatorRect.bottom = b;
    316                 } else {
    317                     indicatorRect.top = t;
    318                     indicatorRect.bottom = b;// + mDividerHeight;
    319                 }
    320 
    321                 // Get the indicator (with its state set to the item's state)
    322                 indicator = getIndicator(pos);
    323                 if (indicator != null) {
    324                     // Draw the indicator
    325                     indicator.setBounds(indicatorRect);
    326                     indicator.draw(canvas);
    327                 }
    328             }
    329             pos.recycle();
    330         }
    331 
    332         if (clipToPadding) {
    333             canvas.restoreToCount(saveCount);
    334         }
    335     }
    336 
    337     /**
    338      * Gets the indicator for the item at the given position. If the indicator
    339      * is stateful, the state will be given to the indicator.
    340      *
    341      * @param pos The flat list position of the item whose indicator
    342      *            should be returned.
    343      * @return The indicator in the proper state.
    344      */
    345     private Drawable getIndicator(PositionMetadata pos) {
    346         Drawable indicator;
    347 
    348         if (pos.position.type == ExpandableListPosition.GROUP) {
    349             indicator = mGroupIndicator;
    350 
    351             if (indicator != null && indicator.isStateful()) {
    352                 // Empty check based on availability of data.  If the groupMetadata isn't null,
    353                 // we do a check on it. Otherwise, the group is collapsed so we consider it
    354                 // empty for performance reasons.
    355                 boolean isEmpty = (pos.groupMetadata == null) ||
    356                         (pos.groupMetadata.lastChildFlPos == pos.groupMetadata.flPos);
    357 
    358                 final int stateSetIndex =
    359                     (pos.isExpanded() ? 1 : 0) | // Expanded?
    360                     (isEmpty ? 2 : 0); // Empty?
    361                 indicator.setState(GROUP_STATE_SETS[stateSetIndex]);
    362             }
    363         } else {
    364             indicator = mChildIndicator;
    365 
    366             if (indicator != null && indicator.isStateful()) {
    367                 // No need for a state sets array for the child since it only has two states
    368                 final int stateSet[] = pos.position.flatListPos == pos.groupMetadata.lastChildFlPos
    369                         ? CHILD_LAST_STATE_SET
    370                         : EMPTY_STATE_SET;
    371                 indicator.setState(stateSet);
    372             }
    373         }
    374 
    375         return indicator;
    376     }
    377 
    378     /**
    379      * Sets the drawable that will be drawn adjacent to every child in the list. This will
    380      * be drawn using the same height as the normal divider ({@link #setDivider(Drawable)}) or
    381      * if it does not have an intrinsic height, the height set by {@link #setDividerHeight(int)}.
    382      *
    383      * @param childDivider The drawable to use.
    384      */
    385     public void setChildDivider(Drawable childDivider) {
    386         mChildDivider = childDivider;
    387     }
    388 
    389     @Override
    390     void drawDivider(Canvas canvas, Rect bounds, int childIndex) {
    391         int flatListPosition = childIndex + mFirstPosition;
    392 
    393         // Only proceed as possible child if the divider isn't above all items (if it is above
    394         // all items, then the item below it has to be a group)
    395         if (flatListPosition >= 0) {
    396             final int adjustedPosition = getFlatPositionForConnector(flatListPosition);
    397             PositionMetadata pos = mConnector.getUnflattenedPos(adjustedPosition);
    398             // If this item is a child, or it is a non-empty group that is expanded
    399             if ((pos.position.type == ExpandableListPosition.CHILD) || (pos.isExpanded() &&
    400                     pos.groupMetadata.lastChildFlPos != pos.groupMetadata.flPos)) {
    401                 // These are the cases where we draw the child divider
    402                 final Drawable divider = mChildDivider;
    403                 divider.setBounds(bounds);
    404                 divider.draw(canvas);
    405                 pos.recycle();
    406                 return;
    407             }
    408             pos.recycle();
    409         }
    410 
    411         // Otherwise draw the default divider
    412         super.drawDivider(canvas, bounds, flatListPosition);
    413     }
    414 
    415     /**
    416      * This overloaded method should not be used, instead use
    417      * {@link #setAdapter(ExpandableListAdapter)}.
    418      * <p>
    419      * {@inheritDoc}
    420      */
    421     @Override
    422     public void setAdapter(ListAdapter adapter) {
    423         throw new RuntimeException(
    424                 "For ExpandableListView, use setAdapter(ExpandableListAdapter) instead of " +
    425                 "setAdapter(ListAdapter)");
    426     }
    427 
    428     /**
    429      * This method should not be used, use {@link #getExpandableListAdapter()}.
    430      */
    431     @Override
    432     public ListAdapter getAdapter() {
    433         /*
    434          * The developer should never really call this method on an
    435          * ExpandableListView, so it would be nice to throw a RuntimeException,
    436          * but AdapterView calls this
    437          */
    438         return super.getAdapter();
    439     }
    440 
    441     /**
    442      * Register a callback to be invoked when an item has been clicked and the
    443      * caller prefers to receive a ListView-style position instead of a group
    444      * and/or child position. In most cases, the caller should use
    445      * {@link #setOnGroupClickListener} and/or {@link #setOnChildClickListener}.
    446      * <p />
    447      * {@inheritDoc}
    448      */
    449     @Override
    450     public void setOnItemClickListener(OnItemClickListener l) {
    451         super.setOnItemClickListener(l);
    452     }
    453 
    454     /**
    455      * Sets the adapter that provides data to this view.
    456      * @param adapter The adapter that provides data to this view.
    457      */
    458     public void setAdapter(ExpandableListAdapter adapter) {
    459         // Set member variable
    460         mAdapter = adapter;
    461 
    462         if (adapter != null) {
    463             // Create the connector
    464             mConnector = new ExpandableListConnector(adapter);
    465         } else {
    466             mConnector = null;
    467         }
    468 
    469         // Link the ListView (superclass) to the expandable list data through the connector
    470         super.setAdapter(mConnector);
    471     }
    472 
    473     /**
    474      * Gets the adapter that provides data to this view.
    475      * @return The adapter that provides data to this view.
    476      */
    477     public ExpandableListAdapter getExpandableListAdapter() {
    478         return mAdapter;
    479     }
    480 
    481     /**
    482      * @param position An absolute (including header and footer) flat list position.
    483      * @return true if the position corresponds to a header or a footer item.
    484      */
    485     private boolean isHeaderOrFooterPosition(int position) {
    486         final int footerViewsStart = mItemCount - getFooterViewsCount();
    487         return (position < getHeaderViewsCount() || position >= footerViewsStart);
    488     }
    489 
    490     /**
    491      * Converts an absolute item flat position into a group/child flat position, shifting according
    492      * to the number of header items.
    493      *
    494      * @param flatListPosition The absolute flat position
    495      * @return A group/child flat position as expected by the connector.
    496      */
    497     private int getFlatPositionForConnector(int flatListPosition) {
    498         return flatListPosition - getHeaderViewsCount();
    499     }
    500 
    501     /**
    502      * Converts a group/child flat position into an absolute flat position, that takes into account
    503      * the possible headers.
    504      *
    505      * @param flatListPosition The child/group flat position
    506      * @return An absolute flat position.
    507      */
    508     private int getAbsoluteFlatPosition(int flatListPosition) {
    509         return flatListPosition + getHeaderViewsCount();
    510     }
    511 
    512     @Override
    513     public boolean performItemClick(View v, int position, long id) {
    514         // Ignore clicks in header/footers
    515         if (isHeaderOrFooterPosition(position)) {
    516             // Clicked on a header/footer, so ignore pass it on to super
    517             return super.performItemClick(v, position, id);
    518         }
    519 
    520         // Internally handle the item click
    521         final int adjustedPosition = getFlatPositionForConnector(position);
    522         return handleItemClick(v, adjustedPosition, id);
    523     }
    524 
    525     /**
    526      * This will either expand/collapse groups (if a group was clicked) or pass
    527      * on the click to the proper child (if a child was clicked)
    528      *
    529      * @param position The flat list position. This has already been factored to
    530      *            remove the header/footer.
    531      * @param id The ListAdapter ID, not the group or child ID.
    532      */
    533     boolean handleItemClick(View v, int position, long id) {
    534         final PositionMetadata posMetadata = mConnector.getUnflattenedPos(position);
    535 
    536         id = getChildOrGroupId(posMetadata.position);
    537 
    538         boolean returnValue;
    539         if (posMetadata.position.type == ExpandableListPosition.GROUP) {
    540             /* It's a group, so handle collapsing/expanding */
    541 
    542             /* It's a group click, so pass on event */
    543             if (mOnGroupClickListener != null) {
    544                 if (mOnGroupClickListener.onGroupClick(this, v,
    545                         posMetadata.position.groupPos, id)) {
    546                     posMetadata.recycle();
    547                     return true;
    548                 }
    549             }
    550 
    551             if (posMetadata.isExpanded()) {
    552                 /* Collapse it */
    553                 mConnector.collapseGroup(posMetadata);
    554 
    555                 playSoundEffect(SoundEffectConstants.CLICK);
    556 
    557                 if (mOnGroupCollapseListener != null) {
    558                     mOnGroupCollapseListener.onGroupCollapse(posMetadata.position.groupPos);
    559                 }
    560             } else {
    561                 /* Expand it */
    562                 mConnector.expandGroup(posMetadata);
    563 
    564                 playSoundEffect(SoundEffectConstants.CLICK);
    565 
    566                 if (mOnGroupExpandListener != null) {
    567                     mOnGroupExpandListener.onGroupExpand(posMetadata.position.groupPos);
    568                 }
    569 
    570                 final int groupPos = posMetadata.position.groupPos;
    571                 final int groupFlatPos = posMetadata.position.flatListPos;
    572 
    573                 final int shiftedGroupPosition = groupFlatPos + getHeaderViewsCount();
    574                 smoothScrollToPosition(shiftedGroupPosition + mAdapter.getChildrenCount(groupPos),
    575                         shiftedGroupPosition);
    576             }
    577 
    578             returnValue = true;
    579         } else {
    580             /* It's a child, so pass on event */
    581             if (mOnChildClickListener != null) {
    582                 playSoundEffect(SoundEffectConstants.CLICK);
    583                 return mOnChildClickListener.onChildClick(this, v, posMetadata.position.groupPos,
    584                         posMetadata.position.childPos, id);
    585             }
    586 
    587             returnValue = false;
    588         }
    589 
    590         posMetadata.recycle();
    591 
    592         return returnValue;
    593     }
    594 
    595     /**
    596      * Expand a group in the grouped list view
    597      *
    598      * @param groupPos the group to be expanded
    599      * @return True if the group was expanded, false otherwise (if the group
    600      *         was already expanded, this will return false)
    601      */
    602     public boolean expandGroup(int groupPos) {
    603        return expandGroup(groupPos, false);
    604     }
    605 
    606     /**
    607      * Expand a group in the grouped list view
    608      *
    609      * @param groupPos the group to be expanded
    610      * @param animate true if the expanding group should be animated in
    611      * @return True if the group was expanded, false otherwise (if the group
    612      *         was already expanded, this will return false)
    613      */
    614     public boolean expandGroup(int groupPos, boolean animate) {
    615         ExpandableListPosition elGroupPos = ExpandableListPosition.obtain(
    616                 ExpandableListPosition.GROUP, groupPos, -1, -1);
    617         PositionMetadata pm = mConnector.getFlattenedPos(elGroupPos);
    618         elGroupPos.recycle();
    619         boolean retValue = mConnector.expandGroup(pm);
    620 
    621         if (mOnGroupExpandListener != null) {
    622             mOnGroupExpandListener.onGroupExpand(groupPos);
    623         }
    624 
    625         if (animate) {
    626             final int groupFlatPos = pm.position.flatListPos;
    627 
    628             final int shiftedGroupPosition = groupFlatPos + getHeaderViewsCount();
    629             smoothScrollToPosition(shiftedGroupPosition + mAdapter.getChildrenCount(groupPos),
    630                     shiftedGroupPosition);
    631         }
    632         pm.recycle();
    633 
    634         return retValue;
    635     }
    636 
    637     /**
    638      * Collapse a group in the grouped list view
    639      *
    640      * @param groupPos position of the group to collapse
    641      * @return True if the group was collapsed, false otherwise (if the group
    642      *         was already collapsed, this will return false)
    643      */
    644     public boolean collapseGroup(int groupPos) {
    645         boolean retValue = mConnector.collapseGroup(groupPos);
    646 
    647         if (mOnGroupCollapseListener != null) {
    648             mOnGroupCollapseListener.onGroupCollapse(groupPos);
    649         }
    650 
    651         return retValue;
    652     }
    653 
    654     /** Used for being notified when a group is collapsed */
    655     public interface OnGroupCollapseListener {
    656         /**
    657          * Callback method to be invoked when a group in this expandable list has
    658          * been collapsed.
    659          *
    660          * @param groupPosition The group position that was collapsed
    661          */
    662         void onGroupCollapse(int groupPosition);
    663     }
    664 
    665     private OnGroupCollapseListener mOnGroupCollapseListener;
    666 
    667     public void setOnGroupCollapseListener(
    668             OnGroupCollapseListener onGroupCollapseListener) {
    669         mOnGroupCollapseListener = onGroupCollapseListener;
    670     }
    671 
    672     /** Used for being notified when a group is expanded */
    673     public interface OnGroupExpandListener {
    674         /**
    675          * Callback method to be invoked when a group in this expandable list has
    676          * been expanded.
    677          *
    678          * @param groupPosition The group position that was expanded
    679          */
    680         void onGroupExpand(int groupPosition);
    681     }
    682 
    683     private OnGroupExpandListener mOnGroupExpandListener;
    684 
    685     public void setOnGroupExpandListener(
    686             OnGroupExpandListener onGroupExpandListener) {
    687         mOnGroupExpandListener = onGroupExpandListener;
    688     }
    689 
    690     /**
    691      * Interface definition for a callback to be invoked when a group in this
    692      * expandable list has been clicked.
    693      */
    694     public interface OnGroupClickListener {
    695         /**
    696          * Callback method to be invoked when a group in this expandable list has
    697          * been clicked.
    698          *
    699          * @param parent The ExpandableListConnector where the click happened
    700          * @param v The view within the expandable list/ListView that was clicked
    701          * @param groupPosition The group position that was clicked
    702          * @param id The row id of the group that was clicked
    703          * @return True if the click was handled
    704          */
    705         boolean onGroupClick(ExpandableListView parent, View v, int groupPosition,
    706                 long id);
    707     }
    708 
    709     private OnGroupClickListener mOnGroupClickListener;
    710 
    711     public void setOnGroupClickListener(OnGroupClickListener onGroupClickListener) {
    712         mOnGroupClickListener = onGroupClickListener;
    713     }
    714 
    715     /**
    716      * Interface definition for a callback to be invoked when a child in this
    717      * expandable list has been clicked.
    718      */
    719     public interface OnChildClickListener {
    720         /**
    721          * Callback method to be invoked when a child in this expandable list has
    722          * been clicked.
    723          *
    724          * @param parent The ExpandableListView where the click happened
    725          * @param v The view within the expandable list/ListView that was clicked
    726          * @param groupPosition The group position that contains the child that
    727          *        was clicked
    728          * @param childPosition The child position within the group
    729          * @param id The row id of the child that was clicked
    730          * @return True if the click was handled
    731          */
    732         boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
    733                 int childPosition, long id);
    734     }
    735 
    736     private OnChildClickListener mOnChildClickListener;
    737 
    738     public void setOnChildClickListener(OnChildClickListener onChildClickListener) {
    739         mOnChildClickListener = onChildClickListener;
    740     }
    741 
    742     /**
    743      * Converts a flat list position (the raw position of an item (child or group)
    744      * in the list) to a group and/or child position (represented in a
    745      * packed position). This is useful in situations where the caller needs to
    746      * use the underlying {@link ListView}'s methods. Use
    747      * {@link ExpandableListView#getPackedPositionType} ,
    748      * {@link ExpandableListView#getPackedPositionChild},
    749      * {@link ExpandableListView#getPackedPositionGroup} to unpack.
    750      *
    751      * @param flatListPosition The flat list position to be converted.
    752      * @return The group and/or child position for the given flat list position
    753      *         in packed position representation. #PACKED_POSITION_VALUE_NULL if
    754      *         the position corresponds to a header or a footer item.
    755      */
    756     public long getExpandableListPosition(int flatListPosition) {
    757         if (isHeaderOrFooterPosition(flatListPosition)) {
    758             return PACKED_POSITION_VALUE_NULL;
    759         }
    760 
    761         final int adjustedPosition = getFlatPositionForConnector(flatListPosition);
    762         PositionMetadata pm = mConnector.getUnflattenedPos(adjustedPosition);
    763         long packedPos = pm.position.getPackedPosition();
    764         pm.recycle();
    765         return packedPos;
    766     }
    767 
    768     /**
    769      * Converts a group and/or child position to a flat list position. This is
    770      * useful in situations where the caller needs to use the underlying
    771      * {@link ListView}'s methods.
    772      *
    773      * @param packedPosition The group and/or child positions to be converted in
    774      *            packed position representation. Use
    775      *            {@link #getPackedPositionForChild(int, int)} or
    776      *            {@link #getPackedPositionForGroup(int)}.
    777      * @return The flat list position for the given child or group.
    778      */
    779     public int getFlatListPosition(long packedPosition) {
    780         ExpandableListPosition elPackedPos = ExpandableListPosition
    781                 .obtainPosition(packedPosition);
    782         PositionMetadata pm = mConnector.getFlattenedPos(elPackedPos);
    783         elPackedPos.recycle();
    784         final int flatListPosition = pm.position.flatListPos;
    785         pm.recycle();
    786         return getAbsoluteFlatPosition(flatListPosition);
    787     }
    788 
    789     /**
    790      * Gets the position of the currently selected group or child (along with
    791      * its type). Can return {@link #PACKED_POSITION_VALUE_NULL} if no selection.
    792      *
    793      * @return A packed position containing the currently selected group or
    794      *         child's position and type. #PACKED_POSITION_VALUE_NULL if no selection
    795      *         or if selection is on a header or a footer item.
    796      */
    797     public long getSelectedPosition() {
    798         final int selectedPos = getSelectedItemPosition();
    799 
    800         // The case where there is no selection (selectedPos == -1) is also handled here.
    801         return getExpandableListPosition(selectedPos);
    802     }
    803 
    804     /**
    805      * Gets the ID of the currently selected group or child. Can return -1 if no
    806      * selection.
    807      *
    808      * @return The ID of the currently selected group or child. -1 if no
    809      *         selection.
    810      */
    811     public long getSelectedId() {
    812         long packedPos = getSelectedPosition();
    813         if (packedPos == PACKED_POSITION_VALUE_NULL) return -1;
    814 
    815         int groupPos = getPackedPositionGroup(packedPos);
    816 
    817         if (getPackedPositionType(packedPos) == PACKED_POSITION_TYPE_GROUP) {
    818             // It's a group
    819             return mAdapter.getGroupId(groupPos);
    820         } else {
    821             // It's a child
    822             return mAdapter.getChildId(groupPos, getPackedPositionChild(packedPos));
    823         }
    824     }
    825 
    826     /**
    827      * Sets the selection to the specified group.
    828      * @param groupPosition The position of the group that should be selected.
    829      */
    830     public void setSelectedGroup(int groupPosition) {
    831         ExpandableListPosition elGroupPos = ExpandableListPosition
    832                 .obtainGroupPosition(groupPosition);
    833         PositionMetadata pm = mConnector.getFlattenedPos(elGroupPos);
    834         elGroupPos.recycle();
    835         final int absoluteFlatPosition = getAbsoluteFlatPosition(pm.position.flatListPos);
    836         super.setSelection(absoluteFlatPosition);
    837         pm.recycle();
    838     }
    839 
    840     /**
    841      * Sets the selection to the specified child. If the child is in a collapsed
    842      * group, the group will only be expanded and child subsequently selected if
    843      * shouldExpandGroup is set to true, otherwise the method will return false.
    844      *
    845      * @param groupPosition The position of the group that contains the child.
    846      * @param childPosition The position of the child within the group.
    847      * @param shouldExpandGroup Whether the child's group should be expanded if
    848      *            it is collapsed.
    849      * @return Whether the selection was successfully set on the child.
    850      */
    851     public boolean setSelectedChild(int groupPosition, int childPosition, boolean shouldExpandGroup) {
    852         ExpandableListPosition elChildPos = ExpandableListPosition.obtainChildPosition(
    853                 groupPosition, childPosition);
    854         PositionMetadata flatChildPos = mConnector.getFlattenedPos(elChildPos);
    855 
    856         if (flatChildPos == null) {
    857             // The child's group isn't expanded
    858 
    859             // Shouldn't expand the group, so return false for we didn't set the selection
    860             if (!shouldExpandGroup) return false;
    861 
    862             expandGroup(groupPosition);
    863 
    864             flatChildPos = mConnector.getFlattenedPos(elChildPos);
    865 
    866             // Sanity check
    867             if (flatChildPos == null) {
    868                 throw new IllegalStateException("Could not find child");
    869             }
    870         }
    871 
    872         int absoluteFlatPosition = getAbsoluteFlatPosition(flatChildPos.position.flatListPos);
    873         super.setSelection(absoluteFlatPosition);
    874 
    875         elChildPos.recycle();
    876         flatChildPos.recycle();
    877 
    878         return true;
    879     }
    880 
    881     /**
    882      * Whether the given group is currently expanded.
    883      *
    884      * @param groupPosition The group to check.
    885      * @return Whether the group is currently expanded.
    886      */
    887     public boolean isGroupExpanded(int groupPosition) {
    888         return mConnector.isGroupExpanded(groupPosition);
    889     }
    890 
    891     /**
    892      * Gets the type of a packed position. See
    893      * {@link #getPackedPositionForChild(int, int)}.
    894      *
    895      * @param packedPosition The packed position for which to return the type.
    896      * @return The type of the position contained within the packed position,
    897      *         either {@link #PACKED_POSITION_TYPE_CHILD}, {@link #PACKED_POSITION_TYPE_GROUP}, or
    898      *         {@link #PACKED_POSITION_TYPE_NULL}.
    899      */
    900     public static int getPackedPositionType(long packedPosition) {
    901         if (packedPosition == PACKED_POSITION_VALUE_NULL) {
    902             return PACKED_POSITION_TYPE_NULL;
    903         }
    904 
    905         return (packedPosition & PACKED_POSITION_MASK_TYPE) == PACKED_POSITION_MASK_TYPE
    906                 ? PACKED_POSITION_TYPE_CHILD
    907                 : PACKED_POSITION_TYPE_GROUP;
    908     }
    909 
    910     /**
    911      * Gets the group position from a packed position. See
    912      * {@link #getPackedPositionForChild(int, int)}.
    913      *
    914      * @param packedPosition The packed position from which the group position
    915      *            will be returned.
    916      * @return The group position portion of the packed position. If this does
    917      *         not contain a group, returns -1.
    918      */
    919     public static int getPackedPositionGroup(long packedPosition) {
    920         // Null
    921         if (packedPosition == PACKED_POSITION_VALUE_NULL) return -1;
    922 
    923         return (int) ((packedPosition & PACKED_POSITION_MASK_GROUP) >> PACKED_POSITION_SHIFT_GROUP);
    924     }
    925 
    926     /**
    927      * Gets the child position from a packed position that is of
    928      * {@link #PACKED_POSITION_TYPE_CHILD} type (use {@link #getPackedPositionType(long)}).
    929      * To get the group that this child belongs to, use
    930      * {@link #getPackedPositionGroup(long)}. See
    931      * {@link #getPackedPositionForChild(int, int)}.
    932      *
    933      * @param packedPosition The packed position from which the child position
    934      *            will be returned.
    935      * @return The child position portion of the packed position. If this does
    936      *         not contain a child, returns -1.
    937      */
    938     public static int getPackedPositionChild(long packedPosition) {
    939         // Null
    940         if (packedPosition == PACKED_POSITION_VALUE_NULL) return -1;
    941 
    942         // Group since a group type clears this bit
    943         if ((packedPosition & PACKED_POSITION_MASK_TYPE) != PACKED_POSITION_MASK_TYPE) return -1;
    944 
    945         return (int) (packedPosition & PACKED_POSITION_MASK_CHILD);
    946     }
    947 
    948     /**
    949      * Returns the packed position representation of a child's position.
    950      * <p>
    951      * In general, a packed position should be used in
    952      * situations where the position given to/returned from an
    953      * {@link ExpandableListAdapter} or {@link ExpandableListView} method can
    954      * either be a child or group. The two positions are packed into a single
    955      * long which can be unpacked using
    956      * {@link #getPackedPositionChild(long)},
    957      * {@link #getPackedPositionGroup(long)}, and
    958      * {@link #getPackedPositionType(long)}.
    959      *
    960      * @param groupPosition The child's parent group's position.
    961      * @param childPosition The child position within the group.
    962      * @return The packed position representation of the child (and parent group).
    963      */
    964     public static long getPackedPositionForChild(int groupPosition, int childPosition) {
    965         return (((long)PACKED_POSITION_TYPE_CHILD) << PACKED_POSITION_SHIFT_TYPE)
    966                 | ((((long)groupPosition) & PACKED_POSITION_INT_MASK_GROUP)
    967                         << PACKED_POSITION_SHIFT_GROUP)
    968                 | (childPosition & PACKED_POSITION_INT_MASK_CHILD);
    969     }
    970 
    971     /**
    972      * Returns the packed position representation of a group's position. See
    973      * {@link #getPackedPositionForChild(int, int)}.
    974      *
    975      * @param groupPosition The child's parent group's position.
    976      * @return The packed position representation of the group.
    977      */
    978     public static long getPackedPositionForGroup(int groupPosition) {
    979         // No need to OR a type in because PACKED_POSITION_GROUP == 0
    980         return ((((long)groupPosition) & PACKED_POSITION_INT_MASK_GROUP)
    981                         << PACKED_POSITION_SHIFT_GROUP);
    982     }
    983 
    984     @Override
    985     ContextMenuInfo createContextMenuInfo(View view, int flatListPosition, long id) {
    986         if (isHeaderOrFooterPosition(flatListPosition)) {
    987             // Return normal info for header/footer view context menus
    988             return new AdapterContextMenuInfo(view, flatListPosition, id);
    989         }
    990 
    991         final int adjustedPosition = getFlatPositionForConnector(flatListPosition);
    992         PositionMetadata pm = mConnector.getUnflattenedPos(adjustedPosition);
    993         ExpandableListPosition pos = pm.position;
    994 
    995         id = getChildOrGroupId(pos);
    996         long packedPosition = pos.getPackedPosition();
    997 
    998         pm.recycle();
    999 
   1000         return new ExpandableListContextMenuInfo(view, packedPosition, id);
   1001     }
   1002 
   1003     /**
   1004      * Gets the ID of the group or child at the given <code>position</code>.
   1005      * This is useful since there is no ListAdapter ID -> ExpandableListAdapter
   1006      * ID conversion mechanism (in some cases, it isn't possible).
   1007      *
   1008      * @param position The position of the child or group whose ID should be
   1009      *            returned.
   1010      */
   1011     private long getChildOrGroupId(ExpandableListPosition position) {
   1012         if (position.type == ExpandableListPosition.CHILD) {
   1013             return mAdapter.getChildId(position.groupPos, position.childPos);
   1014         } else {
   1015             return mAdapter.getGroupId(position.groupPos);
   1016         }
   1017     }
   1018 
   1019     /**
   1020      * Sets the indicator to be drawn next to a child.
   1021      *
   1022      * @param childIndicator The drawable to be used as an indicator. If the
   1023      *            child is the last child for a group, the state
   1024      *            {@link android.R.attr#state_last} will be set.
   1025      */
   1026     public void setChildIndicator(Drawable childIndicator) {
   1027         mChildIndicator = childIndicator;
   1028     }
   1029 
   1030     /**
   1031      * Sets the drawing bounds for the child indicator. For either, you can
   1032      * specify {@link #CHILD_INDICATOR_INHERIT} to use inherit from the general
   1033      * indicator's bounds.
   1034      *
   1035      * @see #setIndicatorBounds(int, int)
   1036      * @param left The left position (relative to the left bounds of this View)
   1037      *            to start drawing the indicator.
   1038      * @param right The right position (relative to the left bounds of this
   1039      *            View) to end the drawing of the indicator.
   1040      */
   1041     public void setChildIndicatorBounds(int left, int right) {
   1042         mChildIndicatorLeft = left;
   1043         mChildIndicatorRight = right;
   1044     }
   1045 
   1046     /**
   1047      * Sets the indicator to be drawn next to a group.
   1048      *
   1049      * @param groupIndicator The drawable to be used as an indicator. If the
   1050      *            group is empty, the state {@link android.R.attr#state_empty} will be
   1051      *            set. If the group is expanded, the state
   1052      *            {@link android.R.attr#state_expanded} will be set.
   1053      */
   1054     public void setGroupIndicator(Drawable groupIndicator) {
   1055         mGroupIndicator = groupIndicator;
   1056         if (mIndicatorRight == 0 && mGroupIndicator != null) {
   1057             mIndicatorRight = mIndicatorLeft + mGroupIndicator.getIntrinsicWidth();
   1058         }
   1059     }
   1060 
   1061     /**
   1062      * Sets the drawing bounds for the indicators (at minimum, the group indicator
   1063      * is affected by this; the child indicator is affected by this if the
   1064      * child indicator bounds are set to inherit).
   1065      *
   1066      * @see #setChildIndicatorBounds(int, int)
   1067      * @param left The left position (relative to the left bounds of this View)
   1068      *            to start drawing the indicator.
   1069      * @param right The right position (relative to the left bounds of this
   1070      *            View) to end the drawing of the indicator.
   1071      */
   1072     public void setIndicatorBounds(int left, int right) {
   1073         mIndicatorLeft = left;
   1074         mIndicatorRight = right;
   1075     }
   1076 
   1077     /**
   1078      * Extra menu information specific to an {@link ExpandableListView} provided
   1079      * to the
   1080      * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) }
   1081      * callback when a context menu is brought up for this AdapterView.
   1082      */
   1083     public static class ExpandableListContextMenuInfo implements ContextMenu.ContextMenuInfo {
   1084 
   1085         public ExpandableListContextMenuInfo(View targetView, long packedPosition, long id) {
   1086             this.targetView = targetView;
   1087             this.packedPosition = packedPosition;
   1088             this.id = id;
   1089         }
   1090 
   1091         /**
   1092          * The view for which the context menu is being displayed. This
   1093          * will be one of the children Views of this {@link ExpandableListView}.
   1094          */
   1095         public View targetView;
   1096 
   1097         /**
   1098          * The packed position in the list represented by the adapter for which
   1099          * the context menu is being displayed. Use the methods
   1100          * {@link ExpandableListView#getPackedPositionType},
   1101          * {@link ExpandableListView#getPackedPositionChild}, and
   1102          * {@link ExpandableListView#getPackedPositionGroup} to unpack this.
   1103          */
   1104         public long packedPosition;
   1105 
   1106         /**
   1107          * The ID of the item (group or child) for which the context menu is
   1108          * being displayed.
   1109          */
   1110         public long id;
   1111     }
   1112 
   1113     static class SavedState extends BaseSavedState {
   1114         ArrayList<ExpandableListConnector.GroupMetadata> expandedGroupMetadataList;
   1115 
   1116         /**
   1117          * Constructor called from {@link ExpandableListView#onSaveInstanceState()}
   1118          */
   1119         SavedState(
   1120                 Parcelable superState,
   1121                 ArrayList<ExpandableListConnector.GroupMetadata> expandedGroupMetadataList) {
   1122             super(superState);
   1123             this.expandedGroupMetadataList = expandedGroupMetadataList;
   1124         }
   1125 
   1126         /**
   1127          * Constructor called from {@link #CREATOR}
   1128          */
   1129         private SavedState(Parcel in) {
   1130             super(in);
   1131             expandedGroupMetadataList = new ArrayList<ExpandableListConnector.GroupMetadata>();
   1132             in.readList(expandedGroupMetadataList, ExpandableListConnector.class.getClassLoader());
   1133         }
   1134 
   1135         @Override
   1136         public void writeToParcel(Parcel out, int flags) {
   1137             super.writeToParcel(out, flags);
   1138             out.writeList(expandedGroupMetadataList);
   1139         }
   1140 
   1141         public static final Parcelable.Creator<SavedState> CREATOR
   1142                 = new Parcelable.Creator<SavedState>() {
   1143             public SavedState createFromParcel(Parcel in) {
   1144                 return new SavedState(in);
   1145             }
   1146 
   1147             public SavedState[] newArray(int size) {
   1148                 return new SavedState[size];
   1149             }
   1150         };
   1151     }
   1152 
   1153     @Override
   1154     public Parcelable onSaveInstanceState() {
   1155         Parcelable superState = super.onSaveInstanceState();
   1156         return new SavedState(superState,
   1157                 mConnector != null ? mConnector.getExpandedGroupMetadataList() : null);
   1158     }
   1159 
   1160     @Override
   1161     public void onRestoreInstanceState(Parcelable state) {
   1162         if (!(state instanceof SavedState)) {
   1163             super.onRestoreInstanceState(state);
   1164             return;
   1165         }
   1166 
   1167         SavedState ss = (SavedState) state;
   1168         super.onRestoreInstanceState(ss.getSuperState());
   1169 
   1170         if (mConnector != null && ss.expandedGroupMetadataList != null) {
   1171             mConnector.setExpandedGroupMetadataList(ss.expandedGroupMetadataList);
   1172         }
   1173     }
   1174 
   1175     @Override
   1176     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
   1177         super.onInitializeAccessibilityEvent(event);
   1178         event.setClassName(ExpandableListView.class.getName());
   1179     }
   1180 
   1181     @Override
   1182     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
   1183         super.onInitializeAccessibilityNodeInfo(info);
   1184         info.setClassName(ExpandableListView.class.getName());
   1185     }
   1186 }
   1187