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