blob: f04f605f3224eef7291e4c89855abb67b6b1de2f [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2007 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
17package android.widget;
18
19import android.database.DataSetObserver;
20import android.os.Parcel;
21import android.os.Parcelable;
22import android.os.SystemClock;
23import android.view.View;
24import android.view.ViewGroup;
25
26import java.util.ArrayList;
27import java.util.Collections;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080028
29/*
30 * Implementation notes:
31 *
32 * <p>
33 * Terminology:
34 * <li> flPos - Flat list position, the position used by ListView
35 * <li> gPos - Group position, the position of a group among all the groups
36 * <li> cPos - Child position, the position of a child among all the children
37 * in a group
38 */
39
40/**
41 * A {@link BaseAdapter} that provides data/Views in an expandable list (offers
42 * features such as collapsing/expanding groups containing children). By
43 * itself, this adapter has no data and is a connector to a
44 * {@link ExpandableListAdapter} which provides the data.
45 * <p>
46 * Internally, this connector translates the flat list position that the
47 * ListAdapter expects to/from group and child positions that the ExpandableListAdapter
48 * expects.
49 */
50class ExpandableListConnector extends BaseAdapter implements Filterable {
51 /**
52 * The ExpandableListAdapter to fetch the data/Views for this expandable list
53 */
54 private ExpandableListAdapter mExpandableListAdapter;
55
56 /**
57 * List of metadata for the currently expanded groups. The metadata consists
58 * of data essential for efficiently translating between flat list positions
59 * and group/child positions. See {@link GroupMetadata}.
60 */
61 private ArrayList<GroupMetadata> mExpGroupMetadataList;
62
63 /** The number of children from all currently expanded groups */
64 private int mTotalExpChildrenCount;
65
66 /** The maximum number of allowable expanded groups. Defaults to 'no limit' */
67 private int mMaxExpGroupCount = Integer.MAX_VALUE;
68
69 /** Change observer used to have ExpandableListAdapter changes pushed to us */
Gilles Debunne272f3a92010-03-03 15:55:13 -080070 private final DataSetObserver mDataSetObserver = new MyDataSetObserver();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080071
72 /**
73 * Constructs the connector
74 */
75 public ExpandableListConnector(ExpandableListAdapter expandableListAdapter) {
76 mExpGroupMetadataList = new ArrayList<GroupMetadata>();
77
78 setExpandableListAdapter(expandableListAdapter);
79 }
80
81 /**
82 * Point to the {@link ExpandableListAdapter} that will give us data/Views
83 *
84 * @param expandableListAdapter the adapter that supplies us with data/Views
85 */
86 public void setExpandableListAdapter(ExpandableListAdapter expandableListAdapter) {
87 if (mExpandableListAdapter != null) {
88 mExpandableListAdapter.unregisterDataSetObserver(mDataSetObserver);
89 }
90
91 mExpandableListAdapter = expandableListAdapter;
92 expandableListAdapter.registerDataSetObserver(mDataSetObserver);
93 }
94
95 /**
96 * Translates a flat list position to either a) group pos if the specified
97 * flat list position corresponds to a group, or b) child pos if it
98 * corresponds to a child. Performs a binary search on the expanded
99 * groups list to find the flat list pos if it is an exp group, otherwise
100 * finds where the flat list pos fits in between the exp groups.
101 *
102 * @param flPos the flat list position to be translated
103 * @return the group position or child position of the specified flat list
104 * position encompassed in a {@link PositionMetadata} object
105 * that contains additional useful info for insertion, etc.
106 */
107 PositionMetadata getUnflattenedPos(final int flPos) {
108 /* Keep locally since frequent use */
109 final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
110 final int numExpGroups = egml.size();
111
112 /* Binary search variables */
113 int leftExpGroupIndex = 0;
114 int rightExpGroupIndex = numExpGroups - 1;
115 int midExpGroupIndex = 0;
116 GroupMetadata midExpGm;
117
118 if (numExpGroups == 0) {
119 /*
120 * There aren't any expanded groups (hence no visible children
121 * either), so flPos must be a group and its group pos will be the
122 * same as its flPos
123 */
124 return PositionMetadata.obtain(flPos, ExpandableListPosition.GROUP, flPos,
125 -1, null, 0);
126 }
127
128 /*
129 * Binary search over the expanded groups to find either the exact
130 * expanded group (if we're looking for a group) or the group that
131 * contains the child we're looking for. If we are looking for a
132 * collapsed group, we will not have a direct match here, but we will
133 * find the expanded group just before the group we're searching for (so
134 * then we can calculate the group position of the group we're searching
135 * for). If there isn't an expanded group prior to the group being
136 * searched for, then the group being searched for's group position is
137 * the same as the flat list position (since there are no children before
138 * it, and all groups before it are collapsed).
139 */
140 while (leftExpGroupIndex <= rightExpGroupIndex) {
141 midExpGroupIndex =
142 (rightExpGroupIndex - leftExpGroupIndex) / 2
143 + leftExpGroupIndex;
144 midExpGm = egml.get(midExpGroupIndex);
145
146 if (flPos > midExpGm.lastChildFlPos) {
147 /*
148 * The flat list position is after the current middle group's
149 * last child's flat list position, so search right
150 */
151 leftExpGroupIndex = midExpGroupIndex + 1;
152 } else if (flPos < midExpGm.flPos) {
153 /*
154 * The flat list position is before the current middle group's
155 * flat list position, so search left
156 */
157 rightExpGroupIndex = midExpGroupIndex - 1;
158 } else if (flPos == midExpGm.flPos) {
159 /*
160 * The flat list position is this middle group's flat list
161 * position, so we've found an exact hit
162 */
163 return PositionMetadata.obtain(flPos, ExpandableListPosition.GROUP,
164 midExpGm.gPos, -1, midExpGm, midExpGroupIndex);
165 } else if (flPos <= midExpGm.lastChildFlPos
166 /* && flPos > midGm.flPos as deduced from previous
167 * conditions */) {
168 /* The flat list position is a child of the middle group */
169
170 /*
171 * Subtract the first child's flat list position from the
172 * specified flat list pos to get the child's position within
173 * the group
174 */
175 final int childPos = flPos - (midExpGm.flPos + 1);
176 return PositionMetadata.obtain(flPos, ExpandableListPosition.CHILD,
177 midExpGm.gPos, childPos, midExpGm, midExpGroupIndex);
178 }
179 }
180
181 /*
182 * If we've reached here, it means the flat list position must be a
183 * group that is not expanded, since otherwise we would have hit it
184 * in the above search.
185 */
186
187
188 /**
189 * If we are to expand this group later, where would it go in the
190 * mExpGroupMetadataList ?
191 */
192 int insertPosition = 0;
193
194 /** What is its group position in the list of all groups? */
195 int groupPos = 0;
196
197 /*
198 * To figure out exact insertion and prior group positions, we need to
199 * determine how we broke out of the binary search. We backtrack
200 * to see this.
201 */
202 if (leftExpGroupIndex > midExpGroupIndex) {
203
204 /*
205 * This would occur in the first conditional, so the flat list
206 * insertion position is after the left group. Also, the
207 * leftGroupPos is one more than it should be (since that broke out
208 * of our binary search), so we decrement it.
209 */
210 final GroupMetadata leftExpGm = egml.get(leftExpGroupIndex-1);
211
212 insertPosition = leftExpGroupIndex;
213
214 /*
215 * Sums the number of groups between the prior exp group and this
216 * one, and then adds it to the prior group's group pos
217 */
218 groupPos =
219 (flPos - leftExpGm.lastChildFlPos) + leftExpGm.gPos;
220 } else if (rightExpGroupIndex < midExpGroupIndex) {
221
222 /*
223 * This would occur in the second conditional, so the flat list
224 * insertion position is before the right group. Also, the
225 * rightGroupPos is one less than it should be, so increment it.
226 */
227 final GroupMetadata rightExpGm = egml.get(++rightExpGroupIndex);
228
229 insertPosition = rightExpGroupIndex;
230
231 /*
232 * Subtracts this group's flat list pos from the group after's flat
233 * list position to find out how many groups are in between the two
234 * groups. Then, subtracts that number from the group after's group
235 * pos to get this group's pos.
236 */
237 groupPos = rightExpGm.gPos - (rightExpGm.flPos - flPos);
238 } else {
239 // TODO: clean exit
240 throw new RuntimeException("Unknown state");
241 }
242
243 return PositionMetadata.obtain(flPos, ExpandableListPosition.GROUP, groupPos, -1,
244 null, insertPosition);
245 }
246
247 /**
248 * Translates either a group pos or a child pos (+ group it belongs to) to a
249 * flat list position. If searching for a child and its group is not expanded, this will
250 * return null since the child isn't being shown in the ListView, and hence it has no
251 * position.
252 *
253 * @param pos a {@link ExpandableListPosition} representing either a group position
254 * or child position
255 * @return the flat list position encompassed in a {@link PositionMetadata}
256 * object that contains additional useful info for insertion, etc., or null.
257 */
258 PositionMetadata getFlattenedPos(final ExpandableListPosition pos) {
259 final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
260 final int numExpGroups = egml.size();
261
262 /* Binary search variables */
263 int leftExpGroupIndex = 0;
264 int rightExpGroupIndex = numExpGroups - 1;
265 int midExpGroupIndex = 0;
266 GroupMetadata midExpGm;
267
268 if (numExpGroups == 0) {
269 /*
270 * There aren't any expanded groups, so flPos must be a group and
271 * its flPos will be the same as its group pos. The
272 * insert position is 0 (since the list is empty).
273 */
274 return PositionMetadata.obtain(pos.groupPos, pos.type,
275 pos.groupPos, pos.childPos, null, 0);
276 }
277
278 /*
279 * Binary search over the expanded groups to find either the exact
280 * expanded group (if we're looking for a group) or the group that
281 * contains the child we're looking for.
282 */
283 while (leftExpGroupIndex <= rightExpGroupIndex) {
284 midExpGroupIndex = (rightExpGroupIndex - leftExpGroupIndex)/2 + leftExpGroupIndex;
285 midExpGm = egml.get(midExpGroupIndex);
286
287 if (pos.groupPos > midExpGm.gPos) {
288 /*
289 * It's after the current middle group, so search right
290 */
291 leftExpGroupIndex = midExpGroupIndex + 1;
292 } else if (pos.groupPos < midExpGm.gPos) {
293 /*
294 * It's before the current middle group, so search left
295 */
296 rightExpGroupIndex = midExpGroupIndex - 1;
297 } else if (pos.groupPos == midExpGm.gPos) {
298 /*
299 * It's this middle group, exact hit
300 */
301
302 if (pos.type == ExpandableListPosition.GROUP) {
303 /* If it's a group, give them this matched group's flPos */
304 return PositionMetadata.obtain(midExpGm.flPos, pos.type,
305 pos.groupPos, pos.childPos, midExpGm, midExpGroupIndex);
306 } else if (pos.type == ExpandableListPosition.CHILD) {
307 /* If it's a child, calculate the flat list pos */
308 return PositionMetadata.obtain(midExpGm.flPos + pos.childPos
309 + 1, pos.type, pos.groupPos, pos.childPos,
310 midExpGm, midExpGroupIndex);
311 } else {
312 return null;
313 }
314 }
315 }
316
317 /*
318 * If we've reached here, it means there was no match in the expanded
319 * groups, so it must be a collapsed group that they're search for
320 */
321 if (pos.type != ExpandableListPosition.GROUP) {
322 /* If it isn't a group, return null */
323 return null;
324 }
325
326 /*
327 * To figure out exact insertion and prior group positions, we need to
328 * determine how we broke out of the binary search. We backtrack to see
329 * this.
330 */
331 if (leftExpGroupIndex > midExpGroupIndex) {
332
333 /*
334 * This would occur in the first conditional, so the flat list
335 * insertion position is after the left group.
336 *
337 * The leftGroupPos is one more than it should be (from the binary
338 * search loop) so we subtract 1 to get the actual left group. Since
339 * the insertion point is AFTER the left group, we keep this +1
340 * value as the insertion point
341 */
342 final GroupMetadata leftExpGm = egml.get(leftExpGroupIndex-1);
343 final int flPos =
344 leftExpGm.lastChildFlPos
345 + (pos.groupPos - leftExpGm.gPos);
346
347 return PositionMetadata.obtain(flPos, pos.type, pos.groupPos,
348 pos.childPos, null, leftExpGroupIndex);
349 } else if (rightExpGroupIndex < midExpGroupIndex) {
350
351 /*
352 * This would occur in the second conditional, so the flat list
353 * insertion position is before the right group. Also, the
354 * rightGroupPos is one less than it should be (from binary search
355 * loop), so we increment to it.
356 */
357 final GroupMetadata rightExpGm = egml.get(++rightExpGroupIndex);
358 final int flPos =
359 rightExpGm.flPos
360 - (rightExpGm.gPos - pos.groupPos);
361 return PositionMetadata.obtain(flPos, pos.type, pos.groupPos,
362 pos.childPos, null, rightExpGroupIndex);
363 } else {
364 return null;
365 }
366 }
367
368 @Override
369 public boolean areAllItemsEnabled() {
370 return mExpandableListAdapter.areAllItemsEnabled();
371 }
372
373 @Override
374 public boolean isEnabled(int flatListPos) {
John Reck820b2362012-04-18 10:41:19 -0700375 final PositionMetadata metadata = getUnflattenedPos(flatListPos);
376 final ExpandableListPosition pos = metadata.position;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800377
378 boolean retValue;
379 if (pos.type == ExpandableListPosition.CHILD) {
380 retValue = mExpandableListAdapter.isChildSelectable(pos.groupPos, pos.childPos);
381 } else {
382 // Groups are always selectable
383 retValue = true;
384 }
385
John Reck820b2362012-04-18 10:41:19 -0700386 metadata.recycle();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800387
388 return retValue;
389 }
390
391 public int getCount() {
392 /*
393 * Total count for the list view is the number groups plus the
394 * number of children from currently expanded groups (a value we keep
395 * cached in this class)
396 */
397 return mExpandableListAdapter.getGroupCount() + mTotalExpChildrenCount;
398 }
399
400 public Object getItem(int flatListPos) {
401 final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
402
403 Object retValue;
404 if (posMetadata.position.type == ExpandableListPosition.GROUP) {
405 retValue = mExpandableListAdapter
406 .getGroup(posMetadata.position.groupPos);
407 } else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
408 retValue = mExpandableListAdapter.getChild(posMetadata.position.groupPos,
409 posMetadata.position.childPos);
410 } else {
411 // TODO: clean exit
412 throw new RuntimeException("Flat list position is of unknown type");
413 }
414
415 posMetadata.recycle();
416
417 return retValue;
418 }
419
420 public long getItemId(int flatListPos) {
421 final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
422 final long groupId = mExpandableListAdapter.getGroupId(posMetadata.position.groupPos);
423
424 long retValue;
425 if (posMetadata.position.type == ExpandableListPosition.GROUP) {
426 retValue = mExpandableListAdapter.getCombinedGroupId(groupId);
427 } else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
428 final long childId = mExpandableListAdapter.getChildId(posMetadata.position.groupPos,
429 posMetadata.position.childPos);
430 retValue = mExpandableListAdapter.getCombinedChildId(groupId, childId);
431 } else {
432 // TODO: clean exit
433 throw new RuntimeException("Flat list position is of unknown type");
434 }
435
436 posMetadata.recycle();
437
438 return retValue;
439 }
440
441 public View getView(int flatListPos, View convertView, ViewGroup parent) {
442 final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
443
444 View retValue;
445 if (posMetadata.position.type == ExpandableListPosition.GROUP) {
Gilles Debunne8340afe2010-03-11 16:25:13 -0800446 retValue = mExpandableListAdapter.getGroupView(posMetadata.position.groupPos,
447 posMetadata.isExpanded(), convertView, parent);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800448 } else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
449 final boolean isLastChild = posMetadata.groupMetadata.lastChildFlPos == flatListPos;
450
451 retValue = mExpandableListAdapter.getChildView(posMetadata.position.groupPos,
452 posMetadata.position.childPos, isLastChild, convertView, parent);
453 } else {
454 // TODO: clean exit
455 throw new RuntimeException("Flat list position is of unknown type");
456 }
457
458 posMetadata.recycle();
459
460 return retValue;
461 }
462
463 @Override
464 public int getItemViewType(int flatListPos) {
John Reck820b2362012-04-18 10:41:19 -0700465 final PositionMetadata metadata = getUnflattenedPos(flatListPos);
466 final ExpandableListPosition pos = metadata.position;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800467
468 int retValue;
Gilles Debunne8340afe2010-03-11 16:25:13 -0800469 if (mExpandableListAdapter instanceof HeterogeneousExpandableList) {
470 HeterogeneousExpandableList adapter =
471 (HeterogeneousExpandableList) mExpandableListAdapter;
472 if (pos.type == ExpandableListPosition.GROUP) {
473 retValue = adapter.getGroupType(pos.groupPos);
474 } else {
475 final int childType = adapter.getChildType(pos.groupPos, pos.childPos);
476 retValue = adapter.getGroupTypeCount() + childType;
477 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800478 } else {
Gilles Debunne8340afe2010-03-11 16:25:13 -0800479 if (pos.type == ExpandableListPosition.GROUP) {
480 retValue = 0;
481 } else {
482 retValue = 1;
483 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800484 }
485
John Reck820b2362012-04-18 10:41:19 -0700486 metadata.recycle();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800487
488 return retValue;
489 }
490
491 @Override
492 public int getViewTypeCount() {
Gilles Debunne8340afe2010-03-11 16:25:13 -0800493 if (mExpandableListAdapter instanceof HeterogeneousExpandableList) {
494 HeterogeneousExpandableList adapter =
495 (HeterogeneousExpandableList) mExpandableListAdapter;
496 return adapter.getGroupTypeCount() + adapter.getChildTypeCount();
497 } else {
498 return 2;
499 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800500 }
501
502 @Override
503 public boolean hasStableIds() {
504 return mExpandableListAdapter.hasStableIds();
505 }
506
507 /**
508 * Traverses the expanded group metadata list and fills in the flat list
509 * positions.
510 *
511 * @param forceChildrenCountRefresh Forces refreshing of the children count
512 * for all expanded groups.
513 * @param syncGroupPositions Whether to search for the group positions
514 * based on the group IDs. This should only be needed when calling
515 * this from an onChanged callback.
516 */
517 @SuppressWarnings("unchecked")
518 private void refreshExpGroupMetadataList(boolean forceChildrenCountRefresh,
519 boolean syncGroupPositions) {
520 final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
521 int egmlSize = egml.size();
522 int curFlPos = 0;
523
524 /* Update child count as we go through */
525 mTotalExpChildrenCount = 0;
526
527 if (syncGroupPositions) {
528 // We need to check whether any groups have moved positions
529 boolean positionsChanged = false;
530
531 for (int i = egmlSize - 1; i >= 0; i--) {
532 GroupMetadata curGm = egml.get(i);
533 int newGPos = findGroupPosition(curGm.gId, curGm.gPos);
534 if (newGPos != curGm.gPos) {
535 if (newGPos == AdapterView.INVALID_POSITION) {
536 // Doh, just remove it from the list of expanded groups
537 egml.remove(i);
538 egmlSize--;
539 }
540
541 curGm.gPos = newGPos;
542 if (!positionsChanged) positionsChanged = true;
543 }
544 }
545
546 if (positionsChanged) {
547 // At least one group changed positions, so re-sort
548 Collections.sort(egml);
549 }
550 }
551
552 int gChildrenCount;
553 int lastGPos = 0;
554 for (int i = 0; i < egmlSize; i++) {
555 /* Store in local variable since we'll access freq */
556 GroupMetadata curGm = egml.get(i);
557
558 /*
559 * Get the number of children, try to refrain from calling
560 * another class's method unless we have to (so do a subtraction)
561 */
562 if ((curGm.lastChildFlPos == GroupMetadata.REFRESH) || forceChildrenCountRefresh) {
563 gChildrenCount = mExpandableListAdapter.getChildrenCount(curGm.gPos);
564 } else {
565 /* Num children for this group is its last child's fl pos minus
566 * the group's fl pos
567 */
568 gChildrenCount = curGm.lastChildFlPos - curGm.flPos;
569 }
570
571 /* Update */
572 mTotalExpChildrenCount += gChildrenCount;
573
574 /*
575 * This skips the collapsed groups and increments the flat list
576 * position (for subsequent exp groups) by accounting for the collapsed
577 * groups
578 */
579 curFlPos += (curGm.gPos - lastGPos);
580 lastGPos = curGm.gPos;
581
582 /* Update the flat list positions, and the current flat list pos */
583 curGm.flPos = curFlPos;
584 curFlPos += gChildrenCount;
585 curGm.lastChildFlPos = curFlPos;
586 }
587 }
588
589 /**
590 * Collapse a group in the grouped list view
591 *
592 * @param groupPos position of the group to collapse
593 */
594 boolean collapseGroup(int groupPos) {
John Reck820b2362012-04-18 10:41:19 -0700595 ExpandableListPosition elGroupPos = ExpandableListPosition.obtain(
596 ExpandableListPosition.GROUP, groupPos, -1, -1);
597 PositionMetadata pm = getFlattenedPos(elGroupPos);
598 elGroupPos.recycle();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800599 if (pm == null) return false;
600
601 boolean retValue = collapseGroup(pm);
602 pm.recycle();
603 return retValue;
604 }
605
606 boolean collapseGroup(PositionMetadata posMetadata) {
607 /*
608 * Collapsing requires removal from mExpGroupMetadataList
609 */
610
611 /*
612 * If it is null, it must be already collapsed. This group metadata
613 * object should have been set from the search that returned the
614 * position metadata object.
615 */
616 if (posMetadata.groupMetadata == null) return false;
617
618 // Remove the group from the list of expanded groups
619 mExpGroupMetadataList.remove(posMetadata.groupMetadata);
620
621 // Refresh the metadata
622 refreshExpGroupMetadataList(false, false);
623
624 // Notify of change
625 notifyDataSetChanged();
626
627 // Give the callback
628 mExpandableListAdapter.onGroupCollapsed(posMetadata.groupMetadata.gPos);
629
630 return true;
631 }
632
633 /**
634 * Expand a group in the grouped list view
635 * @param groupPos the group to be expanded
636 */
637 boolean expandGroup(int groupPos) {
John Reck820b2362012-04-18 10:41:19 -0700638 ExpandableListPosition elGroupPos = ExpandableListPosition.obtain(
639 ExpandableListPosition.GROUP, groupPos, -1, -1);
640 PositionMetadata pm = getFlattenedPos(elGroupPos);
641 elGroupPos.recycle();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800642 boolean retValue = expandGroup(pm);
643 pm.recycle();
644 return retValue;
645 }
646
647 boolean expandGroup(PositionMetadata posMetadata) {
648 /*
649 * Expanding requires insertion into the mExpGroupMetadataList
650 */
651
652 if (posMetadata.position.groupPos < 0) {
653 // TODO clean exit
654 throw new RuntimeException("Need group");
655 }
656
657 if (mMaxExpGroupCount == 0) return false;
658
659 // Check to see if it's already expanded
660 if (posMetadata.groupMetadata != null) return false;
661
Gilles Debunne192ab902010-02-24 15:50:40 -0800662 /* Restrict number of expanded groups to mMaxExpGroupCount */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800663 if (mExpGroupMetadataList.size() >= mMaxExpGroupCount) {
664 /* Collapse a group */
665 // TODO: Collapse something not on the screen instead of the first one?
666 // TODO: Could write overloaded function to take GroupMetadata to collapse
667 GroupMetadata collapsedGm = mExpGroupMetadataList.get(0);
668
669 int collapsedIndex = mExpGroupMetadataList.indexOf(collapsedGm);
670
671 collapseGroup(collapsedGm.gPos);
672
673 /* Decrement index if it is after the group we removed */
674 if (posMetadata.groupInsertIndex > collapsedIndex) {
675 posMetadata.groupInsertIndex--;
676 }
677 }
678
679 GroupMetadata expandedGm = GroupMetadata.obtain(
680 GroupMetadata.REFRESH,
681 GroupMetadata.REFRESH,
682 posMetadata.position.groupPos,
683 mExpandableListAdapter.getGroupId(posMetadata.position.groupPos));
684
685 mExpGroupMetadataList.add(posMetadata.groupInsertIndex, expandedGm);
686
687 // Refresh the metadata
688 refreshExpGroupMetadataList(false, false);
689
690 // Notify of change
691 notifyDataSetChanged();
692
693 // Give the callback
694 mExpandableListAdapter.onGroupExpanded(expandedGm.gPos);
695
696 return true;
697 }
698
699 /**
700 * Whether the given group is currently expanded.
701 * @param groupPosition The group to check.
702 * @return Whether the group is currently expanded.
703 */
704 public boolean isGroupExpanded(int groupPosition) {
705 GroupMetadata groupMetadata;
706 for (int i = mExpGroupMetadataList.size() - 1; i >= 0; i--) {
707 groupMetadata = mExpGroupMetadataList.get(i);
708
709 if (groupMetadata.gPos == groupPosition) {
710 return true;
711 }
712 }
713
714 return false;
715 }
716
717 /**
718 * Set the maximum number of groups that can be expanded at any given time
719 */
720 public void setMaxExpGroupCount(int maxExpGroupCount) {
721 mMaxExpGroupCount = maxExpGroupCount;
722 }
723
724 ExpandableListAdapter getAdapter() {
725 return mExpandableListAdapter;
726 }
727
728 public Filter getFilter() {
729 ExpandableListAdapter adapter = getAdapter();
730 if (adapter instanceof Filterable) {
731 return ((Filterable) adapter).getFilter();
732 } else {
733 return null;
734 }
735 }
736
737 ArrayList<GroupMetadata> getExpandedGroupMetadataList() {
738 return mExpGroupMetadataList;
739 }
740
741 void setExpandedGroupMetadataList(ArrayList<GroupMetadata> expandedGroupMetadataList) {
742
743 if ((expandedGroupMetadataList == null) || (mExpandableListAdapter == null)) {
744 return;
745 }
746
747 // Make sure our current data set is big enough for the previously
748 // expanded groups, if not, ignore this request
749 int numGroups = mExpandableListAdapter.getGroupCount();
750 for (int i = expandedGroupMetadataList.size() - 1; i >= 0; i--) {
751 if (expandedGroupMetadataList.get(i).gPos >= numGroups) {
752 // Doh, for some reason the client doesn't have some of the groups
753 return;
754 }
755 }
756
757 mExpGroupMetadataList = expandedGroupMetadataList;
758 refreshExpGroupMetadataList(true, false);
759 }
760
761 @Override
762 public boolean isEmpty() {
763 ExpandableListAdapter adapter = getAdapter();
764 return adapter != null ? adapter.isEmpty() : true;
765 }
766
767 /**
768 * Searches the expandable list adapter for a group position matching the
769 * given group ID. The search starts at the given seed position and then
770 * alternates between moving up and moving down until 1) we find the right
771 * position, or 2) we run out of time, or 3) we have looked at every
772 * position
773 *
774 * @return Position of the row that matches the given row ID, or
775 * {@link AdapterView#INVALID_POSITION} if it can't be found
776 * @see AdapterView#findSyncPosition()
777 */
778 int findGroupPosition(long groupIdToMatch, int seedGroupPosition) {
779 int count = mExpandableListAdapter.getGroupCount();
780
781 if (count == 0) {
782 return AdapterView.INVALID_POSITION;
783 }
784
785 // If there isn't a selection don't hunt for it
786 if (groupIdToMatch == AdapterView.INVALID_ROW_ID) {
787 return AdapterView.INVALID_POSITION;
788 }
789
790 // Pin seed to reasonable values
791 seedGroupPosition = Math.max(0, seedGroupPosition);
792 seedGroupPosition = Math.min(count - 1, seedGroupPosition);
793
794 long endTime = SystemClock.uptimeMillis() + AdapterView.SYNC_MAX_DURATION_MILLIS;
795
796 long rowId;
797
798 // first position scanned so far
799 int first = seedGroupPosition;
800
801 // last position scanned so far
802 int last = seedGroupPosition;
803
804 // True if we should move down on the next iteration
805 boolean next = false;
806
807 // True when we have looked at the first item in the data
808 boolean hitFirst;
809
810 // True when we have looked at the last item in the data
811 boolean hitLast;
812
813 // Get the item ID locally (instead of getItemIdAtPosition), so
814 // we need the adapter
815 ExpandableListAdapter adapter = getAdapter();
816 if (adapter == null) {
817 return AdapterView.INVALID_POSITION;
818 }
819
820 while (SystemClock.uptimeMillis() <= endTime) {
821 rowId = adapter.getGroupId(seedGroupPosition);
822 if (rowId == groupIdToMatch) {
823 // Found it!
824 return seedGroupPosition;
825 }
826
827 hitLast = last == count - 1;
828 hitFirst = first == 0;
829
830 if (hitLast && hitFirst) {
831 // Looked at everything
832 break;
833 }
834
835 if (hitFirst || (next && !hitLast)) {
836 // Either we hit the top, or we are trying to move down
837 last++;
838 seedGroupPosition = last;
839 // Try going up next time
840 next = false;
841 } else if (hitLast || (!next && !hitFirst)) {
842 // Either we hit the bottom, or we are trying to move up
843 first--;
844 seedGroupPosition = first;
845 // Try going down next time
846 next = true;
847 }
848
849 }
850
851 return AdapterView.INVALID_POSITION;
852 }
853
854 protected class MyDataSetObserver extends DataSetObserver {
855 @Override
856 public void onChanged() {
857 refreshExpGroupMetadataList(true, true);
858
859 notifyDataSetChanged();
860 }
861
862 @Override
863 public void onInvalidated() {
864 refreshExpGroupMetadataList(true, true);
865
866 notifyDataSetInvalidated();
867 }
868 }
869
870 /**
871 * Metadata about an expanded group to help convert from a flat list
872 * position to either a) group position for groups, or b) child position for
873 * children
874 */
Gilles Debunne272f3a92010-03-03 15:55:13 -0800875 static class GroupMetadata implements Parcelable, Comparable<GroupMetadata> {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800876 final static int REFRESH = -1;
877
878 /** This group's flat list position */
879 int flPos;
880
881 /* firstChildFlPos isn't needed since it's (flPos + 1) */
882
883 /**
884 * This group's last child's flat list position, so basically
885 * the range of this group in the flat list
886 */
887 int lastChildFlPos;
888
889 /**
890 * This group's group position
891 */
892 int gPos;
893
894 /**
895 * This group's id
896 */
897 long gId;
898
899 private GroupMetadata() {
900 }
901
902 static GroupMetadata obtain(int flPos, int lastChildFlPos, int gPos, long gId) {
903 GroupMetadata gm = new GroupMetadata();
904 gm.flPos = flPos;
905 gm.lastChildFlPos = lastChildFlPos;
906 gm.gPos = gPos;
907 gm.gId = gId;
908 return gm;
909 }
910
Gilles Debunne272f3a92010-03-03 15:55:13 -0800911 public int compareTo(GroupMetadata another) {
912 if (another == null) {
913 throw new IllegalArgumentException();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800914 }
915
Gilles Debunne272f3a92010-03-03 15:55:13 -0800916 return gPos - another.gPos;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800917 }
918
919 public int describeContents() {
920 return 0;
921 }
922
923 public void writeToParcel(Parcel dest, int flags) {
924 dest.writeInt(flPos);
925 dest.writeInt(lastChildFlPos);
926 dest.writeInt(gPos);
927 dest.writeLong(gId);
928 }
929
Jeff Sharkey9e8f83d2019-02-28 12:06:45 -0700930 public static final @android.annotation.NonNull Parcelable.Creator<GroupMetadata> CREATOR =
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800931 new Parcelable.Creator<GroupMetadata>() {
932
933 public GroupMetadata createFromParcel(Parcel in) {
934 GroupMetadata gm = GroupMetadata.obtain(
935 in.readInt(),
936 in.readInt(),
937 in.readInt(),
938 in.readLong());
939 return gm;
940 }
941
942 public GroupMetadata[] newArray(int size) {
943 return new GroupMetadata[size];
944 }
945 };
946
947 }
948
949 /**
950 * Data type that contains an expandable list position (can refer to either a group
951 * or child) and some extra information regarding referred item (such as
952 * where to insert into the flat list, etc.)
953 */
954 static public class PositionMetadata {
955
956 private static final int MAX_POOL_SIZE = 5;
957 private static ArrayList<PositionMetadata> sPool =
958 new ArrayList<PositionMetadata>(MAX_POOL_SIZE);
959
960 /** Data type to hold the position and its type (child/group) */
961 public ExpandableListPosition position;
962
963 /**
964 * Link back to the expanded GroupMetadata for this group. Useful for
965 * removing the group from the list of expanded groups inside the
966 * connector when we collapse the group, and also as a check to see if
967 * the group was expanded or collapsed (this will be null if the group
968 * is collapsed since we don't keep that group's metadata)
969 */
970 public GroupMetadata groupMetadata;
971
972 /**
973 * For groups that are collapsed, we use this as the index (in
974 * mExpGroupMetadataList) to insert this group when we are expanding
975 * this group.
976 */
977 public int groupInsertIndex;
978
979 private void resetState() {
John Reck820b2362012-04-18 10:41:19 -0700980 if (position != null) {
981 position.recycle();
982 position = null;
983 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800984 groupMetadata = null;
985 groupInsertIndex = 0;
986 }
987
988 /**
989 * Use {@link #obtain(int, int, int, int, GroupMetadata, int)}
990 */
991 private PositionMetadata() {
992 }
993
994 static PositionMetadata obtain(int flatListPos, int type, int groupPos,
995 int childPos, GroupMetadata groupMetadata, int groupInsertIndex) {
996 PositionMetadata pm = getRecycledOrCreate();
997 pm.position = ExpandableListPosition.obtain(type, groupPos, childPos, flatListPos);
998 pm.groupMetadata = groupMetadata;
999 pm.groupInsertIndex = groupInsertIndex;
1000 return pm;
1001 }
1002
1003 private static PositionMetadata getRecycledOrCreate() {
1004 PositionMetadata pm;
1005 synchronized (sPool) {
1006 if (sPool.size() > 0) {
1007 pm = sPool.remove(0);
1008 } else {
1009 return new PositionMetadata();
1010 }
1011 }
1012 pm.resetState();
1013 return pm;
1014 }
1015
1016 public void recycle() {
John Reck820b2362012-04-18 10:41:19 -07001017 resetState();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001018 synchronized (sPool) {
1019 if (sPool.size() < MAX_POOL_SIZE) {
1020 sPool.add(this);
1021 }
1022 }
1023 }
1024
1025 /**
1026 * Checks whether the group referred to in this object is expanded,
1027 * or not (at the time this object was created)
1028 *
1029 * @return whether the group at groupPos is expanded or not
1030 */
1031 public boolean isExpanded() {
1032 return groupMetadata != null;
1033 }
1034 }
1035}