ExpandableListConnector.java revision 272f3a9474a70817f8089259da4c2696732dfecb
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 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; 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 */ 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 */ 70 private final DataSetObserver mDataSetObserver = new MyDataSetObserver(); 71 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) { 375 final ExpandableListPosition pos = getUnflattenedPos(flatListPos).position; 376 377 boolean retValue; 378 if (pos.type == ExpandableListPosition.CHILD) { 379 retValue = mExpandableListAdapter.isChildSelectable(pos.groupPos, pos.childPos); 380 } else { 381 // Groups are always selectable 382 retValue = true; 383 } 384 385 pos.recycle(); 386 387 return retValue; 388 } 389 390 public int getCount() { 391 /* 392 * Total count for the list view is the number groups plus the 393 * number of children from currently expanded groups (a value we keep 394 * cached in this class) 395 */ 396 return mExpandableListAdapter.getGroupCount() + mTotalExpChildrenCount; 397 } 398 399 public Object getItem(int flatListPos) { 400 final PositionMetadata posMetadata = getUnflattenedPos(flatListPos); 401 402 Object retValue; 403 if (posMetadata.position.type == ExpandableListPosition.GROUP) { 404 retValue = mExpandableListAdapter 405 .getGroup(posMetadata.position.groupPos); 406 } else if (posMetadata.position.type == ExpandableListPosition.CHILD) { 407 retValue = mExpandableListAdapter.getChild(posMetadata.position.groupPos, 408 posMetadata.position.childPos); 409 } else { 410 // TODO: clean exit 411 throw new RuntimeException("Flat list position is of unknown type"); 412 } 413 414 posMetadata.recycle(); 415 416 return retValue; 417 } 418 419 public long getItemId(int flatListPos) { 420 final PositionMetadata posMetadata = getUnflattenedPos(flatListPos); 421 final long groupId = mExpandableListAdapter.getGroupId(posMetadata.position.groupPos); 422 423 long retValue; 424 if (posMetadata.position.type == ExpandableListPosition.GROUP) { 425 retValue = mExpandableListAdapter.getCombinedGroupId(groupId); 426 } else if (posMetadata.position.type == ExpandableListPosition.CHILD) { 427 final long childId = mExpandableListAdapter.getChildId(posMetadata.position.groupPos, 428 posMetadata.position.childPos); 429 retValue = mExpandableListAdapter.getCombinedChildId(groupId, childId); 430 } else { 431 // TODO: clean exit 432 throw new RuntimeException("Flat list position is of unknown type"); 433 } 434 435 posMetadata.recycle(); 436 437 return retValue; 438 } 439 440 public View getView(int flatListPos, View convertView, ViewGroup parent) { 441 final PositionMetadata posMetadata = getUnflattenedPos(flatListPos); 442 443 View retValue; 444 if (posMetadata.position.type == ExpandableListPosition.GROUP) { 445 retValue = mExpandableListAdapter.getGroupView(posMetadata.position.groupPos, posMetadata 446 .isExpanded(), convertView, parent); 447 } else if (posMetadata.position.type == ExpandableListPosition.CHILD) { 448 final boolean isLastChild = posMetadata.groupMetadata.lastChildFlPos == flatListPos; 449 450 retValue = mExpandableListAdapter.getChildView(posMetadata.position.groupPos, 451 posMetadata.position.childPos, isLastChild, convertView, parent); 452 } else { 453 // TODO: clean exit 454 throw new RuntimeException("Flat list position is of unknown type"); 455 } 456 457 posMetadata.recycle(); 458 459 return retValue; 460 } 461 462 @Override 463 public int getItemViewType(int flatListPos) { 464 final ExpandableListPosition pos = getUnflattenedPos(flatListPos).position; 465 466 int retValue; 467 if (pos.type == ExpandableListPosition.GROUP) { 468 retValue = 0; 469 } else { 470 retValue = 1; 471 } 472 473 pos.recycle(); 474 475 return retValue; 476 } 477 478 @Override 479 public int getViewTypeCount() { 480 return 2; 481 } 482 483 @Override 484 public boolean hasStableIds() { 485 return mExpandableListAdapter.hasStableIds(); 486 } 487 488 /** 489 * Traverses the expanded group metadata list and fills in the flat list 490 * positions. 491 * 492 * @param forceChildrenCountRefresh Forces refreshing of the children count 493 * for all expanded groups. 494 * @param syncGroupPositions Whether to search for the group positions 495 * based on the group IDs. This should only be needed when calling 496 * this from an onChanged callback. 497 */ 498 @SuppressWarnings("unchecked") 499 private void refreshExpGroupMetadataList(boolean forceChildrenCountRefresh, 500 boolean syncGroupPositions) { 501 final ArrayList<GroupMetadata> egml = mExpGroupMetadataList; 502 int egmlSize = egml.size(); 503 int curFlPos = 0; 504 505 /* Update child count as we go through */ 506 mTotalExpChildrenCount = 0; 507 508 if (syncGroupPositions) { 509 // We need to check whether any groups have moved positions 510 boolean positionsChanged = false; 511 512 for (int i = egmlSize - 1; i >= 0; i--) { 513 GroupMetadata curGm = egml.get(i); 514 int newGPos = findGroupPosition(curGm.gId, curGm.gPos); 515 if (newGPos != curGm.gPos) { 516 if (newGPos == AdapterView.INVALID_POSITION) { 517 // Doh, just remove it from the list of expanded groups 518 egml.remove(i); 519 egmlSize--; 520 } 521 522 curGm.gPos = newGPos; 523 if (!positionsChanged) positionsChanged = true; 524 } 525 } 526 527 if (positionsChanged) { 528 // At least one group changed positions, so re-sort 529 Collections.sort(egml); 530 } 531 } 532 533 int gChildrenCount; 534 int lastGPos = 0; 535 for (int i = 0; i < egmlSize; i++) { 536 /* Store in local variable since we'll access freq */ 537 GroupMetadata curGm = egml.get(i); 538 539 /* 540 * Get the number of children, try to refrain from calling 541 * another class's method unless we have to (so do a subtraction) 542 */ 543 if ((curGm.lastChildFlPos == GroupMetadata.REFRESH) || forceChildrenCountRefresh) { 544 gChildrenCount = mExpandableListAdapter.getChildrenCount(curGm.gPos); 545 } else { 546 /* Num children for this group is its last child's fl pos minus 547 * the group's fl pos 548 */ 549 gChildrenCount = curGm.lastChildFlPos - curGm.flPos; 550 } 551 552 /* Update */ 553 mTotalExpChildrenCount += gChildrenCount; 554 555 /* 556 * This skips the collapsed groups and increments the flat list 557 * position (for subsequent exp groups) by accounting for the collapsed 558 * groups 559 */ 560 curFlPos += (curGm.gPos - lastGPos); 561 lastGPos = curGm.gPos; 562 563 /* Update the flat list positions, and the current flat list pos */ 564 curGm.flPos = curFlPos; 565 curFlPos += gChildrenCount; 566 curGm.lastChildFlPos = curFlPos; 567 } 568 } 569 570 /** 571 * Collapse a group in the grouped list view 572 * 573 * @param groupPos position of the group to collapse 574 */ 575 boolean collapseGroup(int groupPos) { 576 PositionMetadata pm = getFlattenedPos(ExpandableListPosition.obtain( 577 ExpandableListPosition.GROUP, groupPos, -1, -1)); 578 if (pm == null) return false; 579 580 boolean retValue = collapseGroup(pm); 581 pm.recycle(); 582 return retValue; 583 } 584 585 boolean collapseGroup(PositionMetadata posMetadata) { 586 /* 587 * Collapsing requires removal from mExpGroupMetadataList 588 */ 589 590 /* 591 * If it is null, it must be already collapsed. This group metadata 592 * object should have been set from the search that returned the 593 * position metadata object. 594 */ 595 if (posMetadata.groupMetadata == null) return false; 596 597 // Remove the group from the list of expanded groups 598 mExpGroupMetadataList.remove(posMetadata.groupMetadata); 599 600 // Refresh the metadata 601 refreshExpGroupMetadataList(false, false); 602 603 // Notify of change 604 notifyDataSetChanged(); 605 606 // Give the callback 607 mExpandableListAdapter.onGroupCollapsed(posMetadata.groupMetadata.gPos); 608 609 return true; 610 } 611 612 /** 613 * Expand a group in the grouped list view 614 * @param groupPos the group to be expanded 615 */ 616 boolean expandGroup(int groupPos) { 617 PositionMetadata pm = getFlattenedPos(ExpandableListPosition.obtain( 618 ExpandableListPosition.GROUP, groupPos, -1, -1)); 619 boolean retValue = expandGroup(pm); 620 pm.recycle(); 621 return retValue; 622 } 623 624 boolean expandGroup(PositionMetadata posMetadata) { 625 /* 626 * Expanding requires insertion into the mExpGroupMetadataList 627 */ 628 629 if (posMetadata.position.groupPos < 0) { 630 // TODO clean exit 631 throw new RuntimeException("Need group"); 632 } 633 634 if (mMaxExpGroupCount == 0) return false; 635 636 // Check to see if it's already expanded 637 if (posMetadata.groupMetadata != null) return false; 638 639 /* Restrict number of expanded groups to mMaxExpGroupCount */ 640 if (mExpGroupMetadataList.size() >= mMaxExpGroupCount) { 641 /* Collapse a group */ 642 // TODO: Collapse something not on the screen instead of the first one? 643 // TODO: Could write overloaded function to take GroupMetadata to collapse 644 GroupMetadata collapsedGm = mExpGroupMetadataList.get(0); 645 646 int collapsedIndex = mExpGroupMetadataList.indexOf(collapsedGm); 647 648 collapseGroup(collapsedGm.gPos); 649 650 /* Decrement index if it is after the group we removed */ 651 if (posMetadata.groupInsertIndex > collapsedIndex) { 652 posMetadata.groupInsertIndex--; 653 } 654 } 655 656 GroupMetadata expandedGm = GroupMetadata.obtain( 657 GroupMetadata.REFRESH, 658 GroupMetadata.REFRESH, 659 posMetadata.position.groupPos, 660 mExpandableListAdapter.getGroupId(posMetadata.position.groupPos)); 661 662 mExpGroupMetadataList.add(posMetadata.groupInsertIndex, expandedGm); 663 664 // Refresh the metadata 665 refreshExpGroupMetadataList(false, false); 666 667 // Notify of change 668 notifyDataSetChanged(); 669 670 // Give the callback 671 mExpandableListAdapter.onGroupExpanded(expandedGm.gPos); 672 673 return true; 674 } 675 676 /** 677 * Whether the given group is currently expanded. 678 * @param groupPosition The group to check. 679 * @return Whether the group is currently expanded. 680 */ 681 public boolean isGroupExpanded(int groupPosition) { 682 GroupMetadata groupMetadata; 683 for (int i = mExpGroupMetadataList.size() - 1; i >= 0; i--) { 684 groupMetadata = mExpGroupMetadataList.get(i); 685 686 if (groupMetadata.gPos == groupPosition) { 687 return true; 688 } 689 } 690 691 return false; 692 } 693 694 /** 695 * Set the maximum number of groups that can be expanded at any given time 696 */ 697 public void setMaxExpGroupCount(int maxExpGroupCount) { 698 mMaxExpGroupCount = maxExpGroupCount; 699 } 700 701 ExpandableListAdapter getAdapter() { 702 return mExpandableListAdapter; 703 } 704 705 public Filter getFilter() { 706 ExpandableListAdapter adapter = getAdapter(); 707 if (adapter instanceof Filterable) { 708 return ((Filterable) adapter).getFilter(); 709 } else { 710 return null; 711 } 712 } 713 714 ArrayList<GroupMetadata> getExpandedGroupMetadataList() { 715 return mExpGroupMetadataList; 716 } 717 718 void setExpandedGroupMetadataList(ArrayList<GroupMetadata> expandedGroupMetadataList) { 719 720 if ((expandedGroupMetadataList == null) || (mExpandableListAdapter == null)) { 721 return; 722 } 723 724 // Make sure our current data set is big enough for the previously 725 // expanded groups, if not, ignore this request 726 int numGroups = mExpandableListAdapter.getGroupCount(); 727 for (int i = expandedGroupMetadataList.size() - 1; i >= 0; i--) { 728 if (expandedGroupMetadataList.get(i).gPos >= numGroups) { 729 // Doh, for some reason the client doesn't have some of the groups 730 return; 731 } 732 } 733 734 mExpGroupMetadataList = expandedGroupMetadataList; 735 refreshExpGroupMetadataList(true, false); 736 } 737 738 @Override 739 public boolean isEmpty() { 740 ExpandableListAdapter adapter = getAdapter(); 741 return adapter != null ? adapter.isEmpty() : true; 742 } 743 744 /** 745 * Searches the expandable list adapter for a group position matching the 746 * given group ID. The search starts at the given seed position and then 747 * alternates between moving up and moving down until 1) we find the right 748 * position, or 2) we run out of time, or 3) we have looked at every 749 * position 750 * 751 * @return Position of the row that matches the given row ID, or 752 * {@link AdapterView#INVALID_POSITION} if it can't be found 753 * @see AdapterView#findSyncPosition() 754 */ 755 int findGroupPosition(long groupIdToMatch, int seedGroupPosition) { 756 int count = mExpandableListAdapter.getGroupCount(); 757 758 if (count == 0) { 759 return AdapterView.INVALID_POSITION; 760 } 761 762 // If there isn't a selection don't hunt for it 763 if (groupIdToMatch == AdapterView.INVALID_ROW_ID) { 764 return AdapterView.INVALID_POSITION; 765 } 766 767 // Pin seed to reasonable values 768 seedGroupPosition = Math.max(0, seedGroupPosition); 769 seedGroupPosition = Math.min(count - 1, seedGroupPosition); 770 771 long endTime = SystemClock.uptimeMillis() + AdapterView.SYNC_MAX_DURATION_MILLIS; 772 773 long rowId; 774 775 // first position scanned so far 776 int first = seedGroupPosition; 777 778 // last position scanned so far 779 int last = seedGroupPosition; 780 781 // True if we should move down on the next iteration 782 boolean next = false; 783 784 // True when we have looked at the first item in the data 785 boolean hitFirst; 786 787 // True when we have looked at the last item in the data 788 boolean hitLast; 789 790 // Get the item ID locally (instead of getItemIdAtPosition), so 791 // we need the adapter 792 ExpandableListAdapter adapter = getAdapter(); 793 if (adapter == null) { 794 return AdapterView.INVALID_POSITION; 795 } 796 797 while (SystemClock.uptimeMillis() <= endTime) { 798 rowId = adapter.getGroupId(seedGroupPosition); 799 if (rowId == groupIdToMatch) { 800 // Found it! 801 return seedGroupPosition; 802 } 803 804 hitLast = last == count - 1; 805 hitFirst = first == 0; 806 807 if (hitLast && hitFirst) { 808 // Looked at everything 809 break; 810 } 811 812 if (hitFirst || (next && !hitLast)) { 813 // Either we hit the top, or we are trying to move down 814 last++; 815 seedGroupPosition = last; 816 // Try going up next time 817 next = false; 818 } else if (hitLast || (!next && !hitFirst)) { 819 // Either we hit the bottom, or we are trying to move up 820 first--; 821 seedGroupPosition = first; 822 // Try going down next time 823 next = true; 824 } 825 826 } 827 828 return AdapterView.INVALID_POSITION; 829 } 830 831 protected class MyDataSetObserver extends DataSetObserver { 832 @Override 833 public void onChanged() { 834 refreshExpGroupMetadataList(true, true); 835 836 notifyDataSetChanged(); 837 } 838 839 @Override 840 public void onInvalidated() { 841 refreshExpGroupMetadataList(true, true); 842 843 notifyDataSetInvalidated(); 844 } 845 } 846 847 /** 848 * Metadata about an expanded group to help convert from a flat list 849 * position to either a) group position for groups, or b) child position for 850 * children 851 */ 852 static class GroupMetadata implements Parcelable, Comparable<GroupMetadata> { 853 final static int REFRESH = -1; 854 855 /** This group's flat list position */ 856 int flPos; 857 858 /* firstChildFlPos isn't needed since it's (flPos + 1) */ 859 860 /** 861 * This group's last child's flat list position, so basically 862 * the range of this group in the flat list 863 */ 864 int lastChildFlPos; 865 866 /** 867 * This group's group position 868 */ 869 int gPos; 870 871 /** 872 * This group's id 873 */ 874 long gId; 875 876 private GroupMetadata() { 877 } 878 879 static GroupMetadata obtain(int flPos, int lastChildFlPos, int gPos, long gId) { 880 GroupMetadata gm = new GroupMetadata(); 881 gm.flPos = flPos; 882 gm.lastChildFlPos = lastChildFlPos; 883 gm.gPos = gPos; 884 gm.gId = gId; 885 return gm; 886 } 887 888 public int compareTo(GroupMetadata another) { 889 if (another == null) { 890 throw new IllegalArgumentException(); 891 } 892 893 return gPos - another.gPos; 894 } 895 896 public int describeContents() { 897 return 0; 898 } 899 900 public void writeToParcel(Parcel dest, int flags) { 901 dest.writeInt(flPos); 902 dest.writeInt(lastChildFlPos); 903 dest.writeInt(gPos); 904 dest.writeLong(gId); 905 } 906 907 public static final Parcelable.Creator<GroupMetadata> CREATOR = 908 new Parcelable.Creator<GroupMetadata>() { 909 910 public GroupMetadata createFromParcel(Parcel in) { 911 GroupMetadata gm = GroupMetadata.obtain( 912 in.readInt(), 913 in.readInt(), 914 in.readInt(), 915 in.readLong()); 916 return gm; 917 } 918 919 public GroupMetadata[] newArray(int size) { 920 return new GroupMetadata[size]; 921 } 922 }; 923 924 } 925 926 /** 927 * Data type that contains an expandable list position (can refer to either a group 928 * or child) and some extra information regarding referred item (such as 929 * where to insert into the flat list, etc.) 930 */ 931 static public class PositionMetadata { 932 933 private static final int MAX_POOL_SIZE = 5; 934 private static ArrayList<PositionMetadata> sPool = 935 new ArrayList<PositionMetadata>(MAX_POOL_SIZE); 936 937 /** Data type to hold the position and its type (child/group) */ 938 public ExpandableListPosition position; 939 940 /** 941 * Link back to the expanded GroupMetadata for this group. Useful for 942 * removing the group from the list of expanded groups inside the 943 * connector when we collapse the group, and also as a check to see if 944 * the group was expanded or collapsed (this will be null if the group 945 * is collapsed since we don't keep that group's metadata) 946 */ 947 public GroupMetadata groupMetadata; 948 949 /** 950 * For groups that are collapsed, we use this as the index (in 951 * mExpGroupMetadataList) to insert this group when we are expanding 952 * this group. 953 */ 954 public int groupInsertIndex; 955 956 private void resetState() { 957 position = null; 958 groupMetadata = null; 959 groupInsertIndex = 0; 960 } 961 962 /** 963 * Use {@link #obtain(int, int, int, int, GroupMetadata, int)} 964 */ 965 private PositionMetadata() { 966 } 967 968 static PositionMetadata obtain(int flatListPos, int type, int groupPos, 969 int childPos, GroupMetadata groupMetadata, int groupInsertIndex) { 970 PositionMetadata pm = getRecycledOrCreate(); 971 pm.position = ExpandableListPosition.obtain(type, groupPos, childPos, flatListPos); 972 pm.groupMetadata = groupMetadata; 973 pm.groupInsertIndex = groupInsertIndex; 974 return pm; 975 } 976 977 private static PositionMetadata getRecycledOrCreate() { 978 PositionMetadata pm; 979 synchronized (sPool) { 980 if (sPool.size() > 0) { 981 pm = sPool.remove(0); 982 } else { 983 return new PositionMetadata(); 984 } 985 } 986 pm.resetState(); 987 return pm; 988 } 989 990 public void recycle() { 991 synchronized (sPool) { 992 if (sPool.size() < MAX_POOL_SIZE) { 993 sPool.add(this); 994 } 995 } 996 } 997 998 /** 999 * Checks whether the group referred to in this object is expanded, 1000 * or not (at the time this object was created) 1001 * 1002 * @return whether the group at groupPos is expanded or not 1003 */ 1004 public boolean isExpanded() { 1005 return groupMetadata != null; 1006 } 1007 } 1008} 1009