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