AdapterView.java revision b6af533114cfa5a4547990e79e96bd0402c47e72
1/* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.widget; 18 19import android.content.Context; 20import android.database.DataSetObserver; 21import android.os.Handler; 22import android.os.Parcelable; 23import android.os.SystemClock; 24import android.util.AttributeSet; 25import android.util.SparseArray; 26import android.view.ContextMenu; 27import android.view.View; 28import android.view.ViewGroup; 29import android.view.ViewDebug; 30import android.view.SoundEffectConstants; 31import android.view.ContextMenu.ContextMenuInfo; 32 33 34/** 35 * An AdapterView is a view whose children are determined by an {@link Adapter}. 36 * 37 * <p> 38 * See {@link ListView}, {@link GridView}, {@link Spinner} and 39 * {@link Gallery} for commonly used subclasses of AdapterView. 40 */ 41public abstract class AdapterView<T extends Adapter> extends ViewGroup { 42 43 /** 44 * The item view type returned by {@link Adapter#getItemViewType(int)} when 45 * the adapter does not want the item's view recycled. 46 */ 47 public static final int ITEM_VIEW_TYPE_IGNORE = -1; 48 49 /** 50 * The item view type returned by {@link Adapter#getItemViewType(int)} when 51 * the item is a header or footer. 52 */ 53 public static final int ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2; 54 55 /** 56 * The position of the first child displayed 57 */ 58 @ViewDebug.ExportedProperty 59 int mFirstPosition = 0; 60 61 /** 62 * The offset in pixels from the top of the AdapterView to the top 63 * of the view to select during the next layout. 64 */ 65 int mSpecificTop; 66 67 /** 68 * Position from which to start looking for mSyncRowId 69 */ 70 int mSyncPosition; 71 72 /** 73 * Row id to look for when data has changed 74 */ 75 long mSyncRowId = INVALID_ROW_ID; 76 77 /** 78 * Height of the view when mSyncPosition and mSyncRowId where set 79 */ 80 long mSyncHeight; 81 82 /** 83 * True if we need to sync to mSyncRowId 84 */ 85 boolean mNeedSync = false; 86 87 /** 88 * Indicates whether to sync based on the selection or position. Possible 89 * values are {@link #SYNC_SELECTED_POSITION} or 90 * {@link #SYNC_FIRST_POSITION}. 91 */ 92 int mSyncMode; 93 94 /** 95 * Our height after the last layout 96 */ 97 private int mLayoutHeight; 98 99 /** 100 * Sync based on the selected child 101 */ 102 static final int SYNC_SELECTED_POSITION = 0; 103 104 /** 105 * Sync based on the first child displayed 106 */ 107 static final int SYNC_FIRST_POSITION = 1; 108 109 /** 110 * Maximum amount of time to spend in {@link #findSyncPosition()} 111 */ 112 static final int SYNC_MAX_DURATION_MILLIS = 100; 113 114 /** 115 * Indicates that this view is currently being laid out. 116 */ 117 boolean mInLayout = false; 118 119 /** 120 * The listener that receives notifications when an item is selected. 121 */ 122 OnItemSelectedListener mOnItemSelectedListener; 123 124 /** 125 * The listener that receives notifications when an item is clicked. 126 */ 127 OnItemClickListener mOnItemClickListener; 128 129 /** 130 * The listener that receives notifications when an item is long clicked. 131 */ 132 OnItemLongClickListener mOnItemLongClickListener; 133 134 /** 135 * True if the data has changed since the last layout 136 */ 137 boolean mDataChanged; 138 139 /** 140 * The position within the adapter's data set of the item to select 141 * during the next layout. 142 */ 143 @ViewDebug.ExportedProperty 144 int mNextSelectedPosition = INVALID_POSITION; 145 146 /** 147 * The item id of the item to select during the next layout. 148 */ 149 long mNextSelectedRowId = INVALID_ROW_ID; 150 151 /** 152 * The position within the adapter's data set of the currently selected item. 153 */ 154 @ViewDebug.ExportedProperty 155 int mSelectedPosition = INVALID_POSITION; 156 157 /** 158 * The item id of the currently selected item. 159 */ 160 long mSelectedRowId = INVALID_ROW_ID; 161 162 /** 163 * View to show if there are no items to show. 164 */ 165 private View mEmptyView; 166 167 /** 168 * The number of items in the current adapter. 169 */ 170 @ViewDebug.ExportedProperty 171 int mItemCount; 172 173 /** 174 * The number of items in the adapter before a data changed event occured. 175 */ 176 int mOldItemCount; 177 178 /** 179 * Represents an invalid position. All valid positions are in the range 0 to 1 less than the 180 * number of items in the current adapter. 181 */ 182 public static final int INVALID_POSITION = -1; 183 184 /** 185 * Represents an empty or invalid row id 186 */ 187 public static final long INVALID_ROW_ID = Long.MIN_VALUE; 188 189 /** 190 * The last selected position we used when notifying 191 */ 192 int mOldSelectedPosition = INVALID_POSITION; 193 194 /** 195 * The id of the last selected position we used when notifying 196 */ 197 long mOldSelectedRowId = INVALID_ROW_ID; 198 199 /** 200 * Indicates what focusable state is requested when calling setFocusable(). 201 * In addition to this, this view has other criteria for actually 202 * determining the focusable state (such as whether its empty or the text 203 * filter is shown). 204 * 205 * @see #setFocusable(boolean) 206 * @see #checkFocus() 207 */ 208 private boolean mDesiredFocusableState; 209 private boolean mDesiredFocusableInTouchModeState; 210 211 private SelectionNotifier mSelectionNotifier; 212 /** 213 * When set to true, calls to requestLayout() will not propagate up the parent hierarchy. 214 * This is used to layout the children during a layout pass. 215 */ 216 boolean mBlockLayoutRequests = false; 217 218 public AdapterView(Context context) { 219 super(context); 220 } 221 222 public AdapterView(Context context, AttributeSet attrs) { 223 super(context, attrs); 224 } 225 226 public AdapterView(Context context, AttributeSet attrs, int defStyle) { 227 super(context, attrs, defStyle); 228 } 229 230 231 /** 232 * Interface definition for a callback to be invoked when an item in this 233 * AdapterView has been clicked. 234 */ 235 public interface OnItemClickListener { 236 237 /** 238 * Callback method to be invoked when an item in this AdapterView has 239 * been clicked. 240 * <p> 241 * Implementers can call getItemAtPosition(position) if they need 242 * to access the data associated with the selected item. 243 * 244 * @param parent The AdapterView where the click happened. 245 * @param view The view within the AdapterView that was clicked (this 246 * will be a view provided by the adapter) 247 * @param position The position of the view in the adapter. 248 * @param id The row id of the item that was clicked. 249 */ 250 void onItemClick(AdapterView<?> parent, View view, int position, long id); 251 } 252 253 /** 254 * Register a callback to be invoked when an item in this AdapterView has 255 * been clicked. 256 * 257 * @param listener The callback that will be invoked. 258 */ 259 public void setOnItemClickListener(OnItemClickListener listener) { 260 mOnItemClickListener = listener; 261 } 262 263 /** 264 * @return The callback to be invoked with an item in this AdapterView has 265 * been clicked, or null id no callback has been set. 266 */ 267 public final OnItemClickListener getOnItemClickListener() { 268 return mOnItemClickListener; 269 } 270 271 /** 272 * Call the OnItemClickListener, if it is defined. 273 * 274 * @param view The view within the AdapterView that was clicked. 275 * @param position The position of the view in the adapter. 276 * @param id The row id of the item that was clicked. 277 * @return True if there was an assigned OnItemClickListener that was 278 * called, false otherwise is returned. 279 */ 280 public boolean performItemClick(View view, int position, long id) { 281 if (mOnItemClickListener != null) { 282 playSoundEffect(SoundEffectConstants.CLICK); 283 mOnItemClickListener.onItemClick(this, view, position, id); 284 return true; 285 } 286 287 return false; 288 } 289 290 /** 291 * Interface definition for a callback to be invoked when an item in this 292 * view has been clicked and held. 293 */ 294 public interface OnItemLongClickListener { 295 /** 296 * Callback method to be invoked when an item in this view has been 297 * clicked and held. 298 * 299 * Implementers can call getItemAtPosition(position) if they need to access 300 * the data associated with the selected item. 301 * 302 * @param parent The AbsListView where the click happened 303 * @param view The view within the AbsListView that was clicked 304 * @param position The position of the view in the list 305 * @param id The row id of the item that was clicked 306 * 307 * @return true if the callback consumed the long click, false otherwise 308 */ 309 boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id); 310 } 311 312 313 /** 314 * Register a callback to be invoked when an item in this AdapterView has 315 * been clicked and held 316 * 317 * @param listener The callback that will run 318 */ 319 public void setOnItemLongClickListener(OnItemLongClickListener listener) { 320 if (!isLongClickable()) { 321 setLongClickable(true); 322 } 323 mOnItemLongClickListener = listener; 324 } 325 326 /** 327 * @return The callback to be invoked with an item in this AdapterView has 328 * been clicked and held, or null id no callback as been set. 329 */ 330 public final OnItemLongClickListener getOnItemLongClickListener() { 331 return mOnItemLongClickListener; 332 } 333 334 /** 335 * Interface definition for a callback to be invoked when 336 * an item in this view has been selected. 337 */ 338 public interface OnItemSelectedListener { 339 /** 340 * Callback method to be invoked when an item in this view has been 341 * selected. 342 * 343 * Impelmenters can call getItemAtPosition(position) if they need to access the 344 * data associated with the selected item. 345 * 346 * @param parent The AdapterView where the selection happened 347 * @param view The view within the AdapterView that was clicked 348 * @param position The position of the view in the adapter 349 * @param id The row id of the item that is selected 350 */ 351 void onItemSelected(AdapterView<?> parent, View view, int position, long id); 352 353 /** 354 * Callback method to be invoked when the selection disappears from this 355 * view. The selection can disappear for instance when touch is activated 356 * or when the adapter becomes empty. 357 * 358 * @param parent The AdapterView that now contains no selected item. 359 */ 360 void onNothingSelected(AdapterView<?> parent); 361 } 362 363 364 /** 365 * Register a callback to be invoked when an item in this AdapterView has 366 * been selected. 367 * 368 * @param listener The callback that will run 369 */ 370 public void setOnItemSelectedListener(OnItemSelectedListener listener) { 371 mOnItemSelectedListener = listener; 372 } 373 374 public final OnItemSelectedListener getOnItemSelectedListener() { 375 return mOnItemSelectedListener; 376 } 377 378 /** 379 * Extra menu information provided to the 380 * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) } 381 * callback when a context menu is brought up for this AdapterView. 382 * 383 */ 384 public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo { 385 386 public AdapterContextMenuInfo(View targetView, int position, long id) { 387 this.targetView = targetView; 388 this.position = position; 389 this.id = id; 390 } 391 392 /** 393 * The child view for which the context menu is being displayed. This 394 * will be one of the children of this AdapterView. 395 */ 396 public View targetView; 397 398 /** 399 * The position in the adapter for which the context menu is being 400 * displayed. 401 */ 402 public int position; 403 404 /** 405 * The row id of the item for which the context menu is being displayed. 406 */ 407 public long id; 408 } 409 410 /** 411 * Returns the adapter currently associated with this widget. 412 * 413 * @return The adapter used to provide this view's content. 414 */ 415 public abstract T getAdapter(); 416 417 /** 418 * Sets the adapter that provides the data and the views to represent the data 419 * in this widget. 420 * 421 * @param adapter The adapter to use to create this view's content. 422 */ 423 public abstract void setAdapter(T adapter); 424 425 /** 426 * This method is not supported and throws an UnsupportedOperationException when called. 427 * 428 * @param child Ignored. 429 * 430 * @throws UnsupportedOperationException Every time this method is invoked. 431 */ 432 @Override 433 public void addView(View child) { 434 throw new UnsupportedOperationException("addView(View) is not supported in AdapterView"); 435 } 436 437 /** 438 * This method is not supported and throws an UnsupportedOperationException when called. 439 * 440 * @param child Ignored. 441 * @param index Ignored. 442 * 443 * @throws UnsupportedOperationException Every time this method is invoked. 444 */ 445 @Override 446 public void addView(View child, int index) { 447 throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView"); 448 } 449 450 /** 451 * This method is not supported and throws an UnsupportedOperationException when called. 452 * 453 * @param child Ignored. 454 * @param params Ignored. 455 * 456 * @throws UnsupportedOperationException Every time this method is invoked. 457 */ 458 @Override 459 public void addView(View child, LayoutParams params) { 460 throw new UnsupportedOperationException("addView(View, LayoutParams) " 461 + "is not supported in AdapterView"); 462 } 463 464 /** 465 * This method is not supported and throws an UnsupportedOperationException when called. 466 * 467 * @param child Ignored. 468 * @param index Ignored. 469 * @param params Ignored. 470 * 471 * @throws UnsupportedOperationException Every time this method is invoked. 472 */ 473 @Override 474 public void addView(View child, int index, LayoutParams params) { 475 throw new UnsupportedOperationException("addView(View, int, LayoutParams) " 476 + "is not supported in AdapterView"); 477 } 478 479 /** 480 * This method is not supported and throws an UnsupportedOperationException when called. 481 * 482 * @param child Ignored. 483 * 484 * @throws UnsupportedOperationException Every time this method is invoked. 485 */ 486 @Override 487 public void removeView(View child) { 488 throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView"); 489 } 490 491 /** 492 * This method is not supported and throws an UnsupportedOperationException when called. 493 * 494 * @param index Ignored. 495 * 496 * @throws UnsupportedOperationException Every time this method is invoked. 497 */ 498 @Override 499 public void removeViewAt(int index) { 500 throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView"); 501 } 502 503 /** 504 * This method is not supported and throws an UnsupportedOperationException when called. 505 * 506 * @throws UnsupportedOperationException Every time this method is invoked. 507 */ 508 @Override 509 public void removeAllViews() { 510 throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView"); 511 } 512 513 @Override 514 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 515 mLayoutHeight = getHeight(); 516 } 517 518 /** 519 * Return the position of the currently selected item within the adapter's data set 520 * 521 * @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected. 522 */ 523 @ViewDebug.CapturedViewProperty 524 public int getSelectedItemPosition() { 525 return mNextSelectedPosition; 526 } 527 528 /** 529 * @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID} 530 * if nothing is selected. 531 */ 532 @ViewDebug.CapturedViewProperty 533 public long getSelectedItemId() { 534 return mNextSelectedRowId; 535 } 536 537 /** 538 * @return The view corresponding to the currently selected item, or null 539 * if nothing is selected 540 */ 541 public abstract View getSelectedView(); 542 543 /** 544 * @return The data corresponding to the currently selected item, or 545 * null if there is nothing selected. 546 */ 547 public Object getSelectedItem() { 548 T adapter = getAdapter(); 549 int selection = getSelectedItemPosition(); 550 if (adapter != null && adapter.getCount() > 0 && selection >= 0) { 551 return adapter.getItem(selection); 552 } else { 553 return null; 554 } 555 } 556 557 /** 558 * @return The number of items owned by the Adapter associated with this 559 * AdapterView. (This is the number of data items, which may be 560 * larger than the number of visible view.) 561 */ 562 @ViewDebug.CapturedViewProperty 563 public int getCount() { 564 return mItemCount; 565 } 566 567 /** 568 * Get the position within the adapter's data set for the view, where view is a an adapter item 569 * or a descendant of an adapter item. 570 * 571 * @param view an adapter item, or a descendant of an adapter item. This must be visible in this 572 * AdapterView at the time of the call. 573 * @return the position within the adapter's data set of the view, or {@link #INVALID_POSITION} 574 * if the view does not correspond to a list item (or it is not currently visible). 575 */ 576 public int getPositionForView(View view) { 577 View listItem = view; 578 try { 579 View v; 580 while (!(v = (View) listItem.getParent()).equals(this)) { 581 listItem = v; 582 } 583 } catch (ClassCastException e) { 584 // We made it up to the window without find this list view 585 return INVALID_POSITION; 586 } 587 588 // Search the children for the list item 589 final int childCount = getChildCount(); 590 for (int i = 0; i < childCount; i++) { 591 if (getChildAt(i).equals(listItem)) { 592 return mFirstPosition + i; 593 } 594 } 595 596 // Child not found! 597 return INVALID_POSITION; 598 } 599 600 /** 601 * Returns the position within the adapter's data set for the first item 602 * displayed on screen. 603 * 604 * @return The position within the adapter's data set 605 */ 606 public int getFirstVisiblePosition() { 607 return mFirstPosition; 608 } 609 610 /** 611 * Returns the position within the adapter's data set for the last item 612 * displayed on screen. 613 * 614 * @return The position within the adapter's data set 615 */ 616 public int getLastVisiblePosition() { 617 return mFirstPosition + getChildCount() - 1; 618 } 619 620 /** 621 * Sets the currently selected item 622 * @param position Index (starting at 0) of the data item to be selected. 623 */ 624 public abstract void setSelection(int position); 625 626 /** 627 * Sets the view to show if the adapter is empty 628 */ 629 public void setEmptyView(View emptyView) { 630 mEmptyView = emptyView; 631 632 final T adapter = getAdapter(); 633 final boolean empty = ((adapter == null) || adapter.isEmpty()); 634 updateEmptyStatus(empty); 635 } 636 637 /** 638 * When the current adapter is empty, the AdapterView can display a special view 639 * call the empty view. The empty view is used to provide feedback to the user 640 * that no data is available in this AdapterView. 641 * 642 * @return The view to show if the adapter is empty. 643 */ 644 public View getEmptyView() { 645 return mEmptyView; 646 } 647 648 /** 649 * Indicates whether this view is in filter mode. Filter mode can for instance 650 * be enabled by a user when typing on the keyboard. 651 * 652 * @return True if the view is in filter mode, false otherwise. 653 */ 654 boolean isInFilterMode() { 655 return false; 656 } 657 658 @Override 659 public void setFocusable(boolean focusable) { 660 final T adapter = getAdapter(); 661 final boolean empty = adapter == null || adapter.getCount() == 0; 662 663 mDesiredFocusableState = focusable; 664 if (!focusable) { 665 mDesiredFocusableInTouchModeState = false; 666 } 667 668 super.setFocusable(focusable && (!empty || isInFilterMode())); 669 } 670 671 @Override 672 public void setFocusableInTouchMode(boolean focusable) { 673 final T adapter = getAdapter(); 674 final boolean empty = adapter == null || adapter.getCount() == 0; 675 676 mDesiredFocusableInTouchModeState = focusable; 677 if (focusable) { 678 mDesiredFocusableState = true; 679 } 680 681 super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode())); 682 } 683 684 void checkFocus() { 685 final T adapter = getAdapter(); 686 final boolean empty = adapter == null || adapter.getCount() == 0; 687 final boolean focusable = !empty || isInFilterMode(); 688 // The order in which we set focusable in touch mode/focusable may matter 689 // for the client, see View.setFocusableInTouchMode() comments for more 690 // details 691 super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState); 692 super.setFocusable(focusable && mDesiredFocusableState); 693 if (mEmptyView != null) { 694 updateEmptyStatus((adapter == null) || adapter.isEmpty()); 695 } 696 } 697 698 /** 699 * Update the status of the list based on the empty parameter. If empty is true and 700 * we have an empty view, display it. In all the other cases, make sure that the listview 701 * is VISIBLE and that the empty view is GONE (if it's not null). 702 */ 703 private void updateEmptyStatus(boolean empty) { 704 if (isInFilterMode()) { 705 empty = false; 706 } 707 708 if (empty) { 709 if (mEmptyView != null) { 710 mEmptyView.setVisibility(View.VISIBLE); 711 setVisibility(View.GONE); 712 } else { 713 // If the caller just removed our empty view, make sure the list view is visible 714 setVisibility(View.VISIBLE); 715 } 716 717 // We are now GONE, so pending layouts will not be dispatched. 718 // Force one here to make sure that the state of the list matches 719 // the state of the adapter. 720 if (mDataChanged) { 721 this.onLayout(false, mLeft, mTop, mRight, mBottom); 722 } 723 } else { 724 if (mEmptyView != null) mEmptyView.setVisibility(View.GONE); 725 setVisibility(View.VISIBLE); 726 } 727 } 728 729 /** 730 * Gets the data associated with the specified position in the list. 731 * 732 * @param position Which data to get 733 * @return The data associated with the specified position in the list 734 */ 735 public Object getItemAtPosition(int position) { 736 T adapter = getAdapter(); 737 return (adapter == null || position < 0) ? null : adapter.getItem(position); 738 } 739 740 public long getItemIdAtPosition(int position) { 741 T adapter = getAdapter(); 742 return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position); 743 } 744 745 @Override 746 public void setOnClickListener(OnClickListener l) { 747 throw new RuntimeException("Don't call setOnClickListener for an AdapterView. " 748 + "You probably want setOnItemClickListener instead"); 749 } 750 751 /** 752 * Override to prevent freezing of any views created by the adapter. 753 */ 754 @Override 755 protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { 756 dispatchFreezeSelfOnly(container); 757 } 758 759 /** 760 * Override to prevent thawing of any views created by the adapter. 761 */ 762 @Override 763 protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { 764 dispatchThawSelfOnly(container); 765 } 766 767 class AdapterDataSetObserver extends DataSetObserver { 768 769 private Parcelable mInstanceState = null; 770 771 @Override 772 public void onChanged() { 773 mDataChanged = true; 774 mOldItemCount = mItemCount; 775 mItemCount = getAdapter().getCount(); 776 777 // Detect the case where a cursor that was previously invalidated has 778 // been repopulated with new data. 779 if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null 780 && mOldItemCount == 0 && mItemCount > 0) { 781 AdapterView.this.onRestoreInstanceState(mInstanceState); 782 mInstanceState = null; 783 } else { 784 rememberSyncState(); 785 } 786 checkFocus(); 787 requestLayout(); 788 } 789 790 @Override 791 public void onInvalidated() { 792 mDataChanged = true; 793 794 if (AdapterView.this.getAdapter().hasStableIds()) { 795 // Remember the current state for the case where our hosting activity is being 796 // stopped and later restarted 797 mInstanceState = AdapterView.this.onSaveInstanceState(); 798 } 799 800 // Data is invalid so we should reset our state 801 mOldItemCount = mItemCount; 802 mItemCount = 0; 803 mSelectedPosition = INVALID_POSITION; 804 mSelectedRowId = INVALID_ROW_ID; 805 mNextSelectedPosition = INVALID_POSITION; 806 mNextSelectedRowId = INVALID_ROW_ID; 807 mNeedSync = false; 808 checkSelectionChanged(); 809 810 checkFocus(); 811 requestLayout(); 812 } 813 814 public void clearSavedState() { 815 mInstanceState = null; 816 } 817 } 818 819 private class SelectionNotifier extends Handler implements Runnable { 820 public void run() { 821 if (mDataChanged) { 822 // Data has changed between when this SelectionNotifier 823 // was posted and now. We need to wait until the AdapterView 824 // has been synched to the new data. 825 post(this); 826 } else { 827 fireOnSelected(); 828 } 829 } 830 } 831 832 void selectionChanged() { 833 if (mOnItemSelectedListener != null) { 834 if (mInLayout || mBlockLayoutRequests) { 835 // If we are in a layout traversal, defer notification 836 // by posting. This ensures that the view tree is 837 // in a consistent state and is able to accomodate 838 // new layout or invalidate requests. 839 if (mSelectionNotifier == null) { 840 mSelectionNotifier = new SelectionNotifier(); 841 } 842 mSelectionNotifier.post(mSelectionNotifier); 843 } else { 844 fireOnSelected(); 845 } 846 } 847 } 848 849 private void fireOnSelected() { 850 if (mOnItemSelectedListener == null) 851 return; 852 853 int selection = this.getSelectedItemPosition(); 854 if (selection >= 0) { 855 View v = getSelectedView(); 856 mOnItemSelectedListener.onItemSelected(this, v, selection, 857 getAdapter().getItemId(selection)); 858 } else { 859 mOnItemSelectedListener.onNothingSelected(this); 860 } 861 } 862 863 @Override 864 protected boolean canAnimate() { 865 return super.canAnimate() && mItemCount > 0; 866 } 867 868 void handleDataChanged() { 869 final int count = mItemCount; 870 boolean found = false; 871 872 if (count > 0) { 873 874 int newPos; 875 876 // Find the row we are supposed to sync to 877 if (mNeedSync) { 878 // Update this first, since setNextSelectedPositionInt inspects 879 // it 880 mNeedSync = false; 881 882 // See if we can find a position in the new data with the same 883 // id as the old selection 884 newPos = findSyncPosition(); 885 if (newPos >= 0) { 886 // Verify that new selection is selectable 887 int selectablePos = lookForSelectablePosition(newPos, true); 888 if (selectablePos == newPos) { 889 // Same row id is selected 890 setNextSelectedPositionInt(newPos); 891 found = true; 892 } 893 } 894 } 895 if (!found) { 896 // Try to use the same position if we can't find matching data 897 newPos = getSelectedItemPosition(); 898 899 // Pin position to the available range 900 if (newPos >= count) { 901 newPos = count - 1; 902 } 903 if (newPos < 0) { 904 newPos = 0; 905 } 906 907 // Make sure we select something selectable -- first look down 908 int selectablePos = lookForSelectablePosition(newPos, true); 909 if (selectablePos < 0) { 910 // Looking down didn't work -- try looking up 911 selectablePos = lookForSelectablePosition(newPos, false); 912 } 913 if (selectablePos >= 0) { 914 setNextSelectedPositionInt(selectablePos); 915 checkSelectionChanged(); 916 found = true; 917 } 918 } 919 } 920 if (!found) { 921 // Nothing is selected 922 mSelectedPosition = INVALID_POSITION; 923 mSelectedRowId = INVALID_ROW_ID; 924 mNextSelectedPosition = INVALID_POSITION; 925 mNextSelectedRowId = INVALID_ROW_ID; 926 mNeedSync = false; 927 checkSelectionChanged(); 928 } 929 } 930 931 void checkSelectionChanged() { 932 if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) { 933 selectionChanged(); 934 mOldSelectedPosition = mSelectedPosition; 935 mOldSelectedRowId = mSelectedRowId; 936 } 937 } 938 939 /** 940 * Searches the adapter for a position matching mSyncRowId. The search starts at mSyncPosition 941 * and then alternates between moving up and moving down until 1) we find the right position, or 942 * 2) we run out of time, or 3) we have looked at every position 943 * 944 * @return Position of the row that matches mSyncRowId, or {@link #INVALID_POSITION} if it can't 945 * be found 946 */ 947 int findSyncPosition() { 948 int count = mItemCount; 949 950 if (count == 0) { 951 return INVALID_POSITION; 952 } 953 954 long idToMatch = mSyncRowId; 955 int seed = mSyncPosition; 956 957 // If there isn't a selection don't hunt for it 958 if (idToMatch == INVALID_ROW_ID) { 959 return INVALID_POSITION; 960 } 961 962 // Pin seed to reasonable values 963 seed = Math.max(0, seed); 964 seed = Math.min(count - 1, seed); 965 966 long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS; 967 968 long rowId; 969 970 // first position scanned so far 971 int first = seed; 972 973 // last position scanned so far 974 int last = seed; 975 976 // True if we should move down on the next iteration 977 boolean next = false; 978 979 // True when we have looked at the first item in the data 980 boolean hitFirst; 981 982 // True when we have looked at the last item in the data 983 boolean hitLast; 984 985 // Get the item ID locally (instead of getItemIdAtPosition), so 986 // we need the adapter 987 T adapter = getAdapter(); 988 if (adapter == null) { 989 return INVALID_POSITION; 990 } 991 992 while (SystemClock.uptimeMillis() <= endTime) { 993 rowId = adapter.getItemId(seed); 994 if (rowId == idToMatch) { 995 // Found it! 996 return seed; 997 } 998 999 hitLast = last == count - 1; 1000 hitFirst = first == 0; 1001 1002 if (hitLast && hitFirst) { 1003 // Looked at everything 1004 break; 1005 } 1006 1007 if (hitFirst || (next && !hitLast)) { 1008 // Either we hit the top, or we are trying to move down 1009 last++; 1010 seed = last; 1011 // Try going up next time 1012 next = false; 1013 } else if (hitLast || (!next && !hitFirst)) { 1014 // Either we hit the bottom, or we are trying to move up 1015 first--; 1016 seed = first; 1017 // Try going down next time 1018 next = true; 1019 } 1020 1021 } 1022 1023 return INVALID_POSITION; 1024 } 1025 1026 /** 1027 * Find a position that can be selected (i.e., is not a separator). 1028 * 1029 * @param position The starting position to look at. 1030 * @param lookDown Whether to look down for other positions. 1031 * @return The next selectable position starting at position and then searching either up or 1032 * down. Returns {@link #INVALID_POSITION} if nothing can be found. 1033 */ 1034 int lookForSelectablePosition(int position, boolean lookDown) { 1035 return position; 1036 } 1037 1038 /** 1039 * Utility to keep mSelectedPosition and mSelectedRowId in sync 1040 * @param position Our current position 1041 */ 1042 void setSelectedPositionInt(int position) { 1043 mSelectedPosition = position; 1044 mSelectedRowId = getItemIdAtPosition(position); 1045 } 1046 1047 /** 1048 * Utility to keep mNextSelectedPosition and mNextSelectedRowId in sync 1049 * @param position Intended value for mSelectedPosition the next time we go 1050 * through layout 1051 */ 1052 void setNextSelectedPositionInt(int position) { 1053 mNextSelectedPosition = position; 1054 mNextSelectedRowId = getItemIdAtPosition(position); 1055 // If we are trying to sync to the selection, update that too 1056 if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) { 1057 mSyncPosition = position; 1058 mSyncRowId = mNextSelectedRowId; 1059 } 1060 } 1061 1062 /** 1063 * Remember enough information to restore the screen state when the data has 1064 * changed. 1065 * 1066 */ 1067 void rememberSyncState() { 1068 if (getChildCount() > 0) { 1069 mNeedSync = true; 1070 mSyncHeight = mLayoutHeight; 1071 if (mSelectedPosition >= 0) { 1072 // Sync the selection state 1073 View v = getChildAt(mSelectedPosition - mFirstPosition); 1074 mSyncRowId = mNextSelectedRowId; 1075 mSyncPosition = mNextSelectedPosition; 1076 if (v != null) { 1077 mSpecificTop = v.getTop(); 1078 } 1079 mSyncMode = SYNC_SELECTED_POSITION; 1080 } else { 1081 // Sync the based on the offset of the first view 1082 View v = getChildAt(0); 1083 T adapter = getAdapter(); 1084 if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) { 1085 mSyncRowId = adapter.getItemId(mFirstPosition); 1086 } else { 1087 mSyncRowId = NO_ID; 1088 } 1089 mSyncPosition = mFirstPosition; 1090 if (v != null) { 1091 mSpecificTop = v.getTop(); 1092 } 1093 mSyncMode = SYNC_FIRST_POSITION; 1094 } 1095 } 1096 } 1097} 1098