1 /* 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 17 package android.widget; 18 19 import android.database.DataSetObserver; 20 import android.os.Parcel; 21 import android.os.Parcelable; 22 import android.os.SystemClock; 23 import android.view.View; 24 import android.view.ViewGroup; 25 26 import java.util.ArrayList; 27 import java.util.Collections; 28 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 */ 50 class 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 */ 70 private final DataSetObserver mDataSetObserver = new MyDataSetObserver(); 71 72 /** 73 * Constructs the connector 74 */ ExpandableListConnector(ExpandableListAdapter expandableListAdapter)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 */ setExpandableListAdapter(ExpandableListAdapter expandableListAdapter)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 */ getUnflattenedPos(final int flPos)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 */ getFlattenedPos(final ExpandableListPosition pos)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 areAllItemsEnabled()369 public boolean areAllItemsEnabled() { 370 return mExpandableListAdapter.areAllItemsEnabled(); 371 } 372 373 @Override isEnabled(int flatListPos)374 public boolean isEnabled(int flatListPos) { 375 final PositionMetadata metadata = getUnflattenedPos(flatListPos); 376 final ExpandableListPosition pos = metadata.position; 377 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 386 metadata.recycle(); 387 388 return retValue; 389 } 390 getCount()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 getItem(int flatListPos)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 getItemId(int flatListPos)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 getView(int flatListPos, View convertView, ViewGroup parent)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) { 446 retValue = mExpandableListAdapter.getGroupView(posMetadata.position.groupPos, 447 posMetadata.isExpanded(), convertView, parent); 448 } 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 getItemViewType(int flatListPos)464 public int getItemViewType(int flatListPos) { 465 final PositionMetadata metadata = getUnflattenedPos(flatListPos); 466 final ExpandableListPosition pos = metadata.position; 467 468 int retValue; 469 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 } 478 } else { 479 if (pos.type == ExpandableListPosition.GROUP) { 480 retValue = 0; 481 } else { 482 retValue = 1; 483 } 484 } 485 486 metadata.recycle(); 487 488 return retValue; 489 } 490 491 @Override getViewTypeCount()492 public int getViewTypeCount() { 493 if (mExpandableListAdapter instanceof HeterogeneousExpandableList) { 494 HeterogeneousExpandableList adapter = 495 (HeterogeneousExpandableList) mExpandableListAdapter; 496 return adapter.getGroupTypeCount() + adapter.getChildTypeCount(); 497 } else { 498 return 2; 499 } 500 } 501 502 @Override hasStableIds()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") refreshExpGroupMetadataList(boolean forceChildrenCountRefresh, boolean syncGroupPositions)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 */ collapseGroup(int groupPos)594 boolean collapseGroup(int groupPos) { 595 ExpandableListPosition elGroupPos = ExpandableListPosition.obtain( 596 ExpandableListPosition.GROUP, groupPos, -1, -1); 597 PositionMetadata pm = getFlattenedPos(elGroupPos); 598 elGroupPos.recycle(); 599 if (pm == null) return false; 600 601 boolean retValue = collapseGroup(pm); 602 pm.recycle(); 603 return retValue; 604 } 605 collapseGroup(PositionMetadata posMetadata)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 */ expandGroup(int groupPos)637 boolean expandGroup(int groupPos) { 638 ExpandableListPosition elGroupPos = ExpandableListPosition.obtain( 639 ExpandableListPosition.GROUP, groupPos, -1, -1); 640 PositionMetadata pm = getFlattenedPos(elGroupPos); 641 elGroupPos.recycle(); 642 boolean retValue = expandGroup(pm); 643 pm.recycle(); 644 return retValue; 645 } 646 expandGroup(PositionMetadata posMetadata)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 662 /* Restrict number of expanded groups to mMaxExpGroupCount */ 663 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 */ isGroupExpanded(int groupPosition)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 */ setMaxExpGroupCount(int maxExpGroupCount)720 public void setMaxExpGroupCount(int maxExpGroupCount) { 721 mMaxExpGroupCount = maxExpGroupCount; 722 } 723 getAdapter()724 ExpandableListAdapter getAdapter() { 725 return mExpandableListAdapter; 726 } 727 getFilter()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 getExpandedGroupMetadataList()737 ArrayList<GroupMetadata> getExpandedGroupMetadataList() { 738 return mExpGroupMetadataList; 739 } 740 setExpandedGroupMetadataList(ArrayList<GroupMetadata> expandedGroupMetadataList)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 isEmpty()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 */ findGroupPosition(long groupIdToMatch, int seedGroupPosition)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 onChanged()856 public void onChanged() { 857 refreshExpGroupMetadataList(true, true); 858 859 notifyDataSetChanged(); 860 } 861 862 @Override onInvalidated()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 */ 875 static class GroupMetadata implements Parcelable, Comparable<GroupMetadata> { 876 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 GroupMetadata()899 private GroupMetadata() { 900 } 901 obtain(int flPos, int lastChildFlPos, int gPos, long gId)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 compareTo(GroupMetadata another)911 public int compareTo(GroupMetadata another) { 912 if (another == null) { 913 throw new IllegalArgumentException(); 914 } 915 916 return gPos - another.gPos; 917 } 918 describeContents()919 public int describeContents() { 920 return 0; 921 } 922 writeToParcel(Parcel dest, int flags)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 930 public static final @android.annotation.NonNull Parcelable.Creator<GroupMetadata> CREATOR = 931 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 resetState()979 private void resetState() { 980 if (position != null) { 981 position.recycle(); 982 position = null; 983 } 984 groupMetadata = null; 985 groupInsertIndex = 0; 986 } 987 988 /** 989 * Use {@link #obtain(int, int, int, int, GroupMetadata, int)} 990 */ PositionMetadata()991 private PositionMetadata() { 992 } 993 obtain(int flatListPos, int type, int groupPos, int childPos, GroupMetadata groupMetadata, int groupInsertIndex)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 getRecycledOrCreate()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 recycle()1016 public void recycle() { 1017 resetState(); 1018 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 */ isExpanded()1031 public boolean isExpanded() { 1032 return groupMetadata != null; 1033 } 1034 } 1035 } 1036