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