AdapterView.java revision f18c8ddd0f466de4b2f819fbbd612949bd71c532
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.Parcelable; 22import android.os.SystemClock; 23import android.util.AttributeSet; 24import android.util.SparseArray; 25import android.view.ContextMenu; 26import android.view.ContextMenu.ContextMenuInfo; 27import android.view.SoundEffectConstants; 28import android.view.View; 29import android.view.ViewDebug; 30import android.view.ViewGroup; 31import android.view.accessibility.AccessibilityEvent; 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(category = "scrolling") 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(category = "list") 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(category = "list") 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(category = "list") 171 int mItemCount; 172 173 /** 174 * The number of items in the adapter before a data changed event occurred. 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 * Interface definition for a callback to be invoked when an item in this 232 * AdapterView has been clicked. 233 */ 234 public interface OnItemClickListener { 235 236 /** 237 * Callback method to be invoked when an item in this AdapterView has 238 * been clicked. 239 * <p> 240 * Implementers can call getItemAtPosition(position) if they need 241 * to access the data associated with the selected item. 242 * 243 * @param parent The AdapterView where the click happened. 244 * @param view The view within the AdapterView that was clicked (this 245 * will be a view provided by the adapter) 246 * @param position The position of the view in the adapter. 247 * @param id The row id of the item that was clicked. 248 */ 249 void onItemClick(AdapterView<?> parent, View view, int position, long id); 250 } 251 252 /** 253 * Register a callback to be invoked when an item in this AdapterView has 254 * been clicked. 255 * 256 * @param listener The callback that will be invoked. 257 */ 258 public void setOnItemClickListener(OnItemClickListener listener) { 259 mOnItemClickListener = listener; 260 } 261 262 /** 263 * @return The callback to be invoked with an item in this AdapterView has 264 * been clicked, or null id no callback has been set. 265 */ 266 public final OnItemClickListener getOnItemClickListener() { 267 return mOnItemClickListener; 268 } 269 270 /** 271 * Call the OnItemClickListener, if it is defined. 272 * 273 * @param view The view within the AdapterView that was clicked. 274 * @param position The position of the view in the adapter. 275 * @param id The row id of the item that was clicked. 276 * @return True if there was an assigned OnItemClickListener that was 277 * called, false otherwise is returned. 278 */ 279 public boolean performItemClick(View view, int position, long id) { 280 view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 281 282 if (mOnItemClickListener != null) { 283 playSoundEffect(SoundEffectConstants.CLICK); 284 mOnItemClickListener.onItemClick(this, view, position, id); 285 return true; 286 } 287 288 return false; 289 } 290 291 /** 292 * Interface definition for a callback to be invoked when an item in this 293 * view has been clicked and held. 294 */ 295 public interface OnItemLongClickListener { 296 /** 297 * Callback method to be invoked when an item in this view has been 298 * clicked and held. 299 * 300 * Implementers can call getItemAtPosition(position) if they need to access 301 * the data associated with the selected item. 302 * 303 * @param parent The AbsListView where the click happened 304 * @param view The view within the AbsListView that was clicked 305 * @param position The position of the view in the list 306 * @param id The row id of the item that was clicked 307 * 308 * @return true if the callback consumed the long click, false otherwise 309 */ 310 boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id); 311 } 312 313 314 /** 315 * Register a callback to be invoked when an item in this AdapterView has 316 * been clicked and held 317 * 318 * @param listener The callback that will run 319 */ 320 public void setOnItemLongClickListener(OnItemLongClickListener listener) { 321 if (!isLongClickable()) { 322 setLongClickable(true); 323 } 324 mOnItemLongClickListener = listener; 325 } 326 327 /** 328 * @return The callback to be invoked with an item in this AdapterView has 329 * been clicked and held, or null id no callback as been set. 330 */ 331 public final OnItemLongClickListener getOnItemLongClickListener() { 332 return mOnItemLongClickListener; 333 } 334 335 /** 336 * Interface definition for a callback to be invoked when 337 * an item in this view has been selected. 338 */ 339 public interface OnItemSelectedListener { 340 /** 341 * <p>Callback method to be invoked when an item in this view has been 342 * selected. This callback is invoked only when the newly selected 343 * position is different from the previously selected position or if 344 * there was no selected item.</p> 345 * 346 * Impelmenters can call getItemAtPosition(position) if they need to access the 347 * data associated with the selected item. 348 * 349 * @param parent The AdapterView where the selection happened 350 * @param view The view within the AdapterView that was clicked 351 * @param position The position of the view in the adapter 352 * @param id The row id of the item that is selected 353 */ 354 void onItemSelected(AdapterView<?> parent, View view, int position, long id); 355 356 /** 357 * Callback method to be invoked when the selection disappears from this 358 * view. The selection can disappear for instance when touch is activated 359 * or when the adapter becomes empty. 360 * 361 * @param parent The AdapterView that now contains no selected item. 362 */ 363 void onNothingSelected(AdapterView<?> parent); 364 } 365 366 367 /** 368 * Register a callback to be invoked when an item in this AdapterView has 369 * been selected. 370 * 371 * @param listener The callback that will run 372 */ 373 public void setOnItemSelectedListener(OnItemSelectedListener listener) { 374 mOnItemSelectedListener = listener; 375 } 376 377 public final OnItemSelectedListener getOnItemSelectedListener() { 378 return mOnItemSelectedListener; 379 } 380 381 /** 382 * Extra menu information provided to the 383 * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) } 384 * callback when a context menu is brought up for this AdapterView. 385 * 386 */ 387 public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo { 388 389 public AdapterContextMenuInfo(View targetView, int position, long id) { 390 this.targetView = targetView; 391 this.position = position; 392 this.id = id; 393 } 394 395 /** 396 * The child view for which the context menu is being displayed. This 397 * will be one of the children of this AdapterView. 398 */ 399 public View targetView; 400 401 /** 402 * The position in the adapter for which the context menu is being 403 * displayed. 404 */ 405 public int position; 406 407 /** 408 * The row id of the item for which the context menu is being displayed. 409 */ 410 public long id; 411 } 412 413 /** 414 * Returns the adapter currently associated with this widget. 415 * 416 * @return The adapter used to provide this view's content. 417 */ 418 public abstract T getAdapter(); 419 420 /** 421 * Sets the adapter that provides the data and the views to represent the data 422 * in this widget. 423 * 424 * @param adapter The adapter to use to create this view's content. 425 */ 426 public abstract void setAdapter(T adapter); 427 428 /** 429 * This method is not supported and throws an UnsupportedOperationException when called. 430 * 431 * @param child Ignored. 432 * 433 * @throws UnsupportedOperationException Every time this method is invoked. 434 */ 435 @Override 436 public void addView(View child) { 437 throw new UnsupportedOperationException("addView(View) is not supported in AdapterView"); 438 } 439 440 /** 441 * This method is not supported and throws an UnsupportedOperationException when called. 442 * 443 * @param child Ignored. 444 * @param index Ignored. 445 * 446 * @throws UnsupportedOperationException Every time this method is invoked. 447 */ 448 @Override 449 public void addView(View child, int index) { 450 throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView"); 451 } 452 453 /** 454 * This method is not supported and throws an UnsupportedOperationException when called. 455 * 456 * @param child Ignored. 457 * @param params Ignored. 458 * 459 * @throws UnsupportedOperationException Every time this method is invoked. 460 */ 461 @Override 462 public void addView(View child, LayoutParams params) { 463 throw new UnsupportedOperationException("addView(View, LayoutParams) " 464 + "is not supported in AdapterView"); 465 } 466 467 /** 468 * This method is not supported and throws an UnsupportedOperationException when called. 469 * 470 * @param child Ignored. 471 * @param index Ignored. 472 * @param params Ignored. 473 * 474 * @throws UnsupportedOperationException Every time this method is invoked. 475 */ 476 @Override 477 public void addView(View child, int index, LayoutParams params) { 478 throw new UnsupportedOperationException("addView(View, int, LayoutParams) " 479 + "is not supported in AdapterView"); 480 } 481 482 /** 483 * This method is not supported and throws an UnsupportedOperationException when called. 484 * 485 * @param child Ignored. 486 * 487 * @throws UnsupportedOperationException Every time this method is invoked. 488 */ 489 @Override 490 public void removeView(View child) { 491 throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView"); 492 } 493 494 /** 495 * This method is not supported and throws an UnsupportedOperationException when called. 496 * 497 * @param index Ignored. 498 * 499 * @throws UnsupportedOperationException Every time this method is invoked. 500 */ 501 @Override 502 public void removeViewAt(int index) { 503 throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView"); 504 } 505 506 /** 507 * This method is not supported and throws an UnsupportedOperationException when called. 508 * 509 * @throws UnsupportedOperationException Every time this method is invoked. 510 */ 511 @Override 512 public void removeAllViews() { 513 throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView"); 514 } 515 516 @Override 517 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 518 mLayoutHeight = getHeight(); 519 } 520 521 /** 522 * Return the position of the currently selected item within the adapter's data set 523 * 524 * @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected. 525 */ 526 @ViewDebug.CapturedViewProperty 527 public int getSelectedItemPosition() { 528 return mNextSelectedPosition; 529 } 530 531 /** 532 * @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID} 533 * if nothing is selected. 534 */ 535 @ViewDebug.CapturedViewProperty 536 public long getSelectedItemId() { 537 return mNextSelectedRowId; 538 } 539 540 /** 541 * @return The view corresponding to the currently selected item, or null 542 * if nothing is selected 543 */ 544 public abstract View getSelectedView(); 545 546 /** 547 * @return The data corresponding to the currently selected item, or 548 * null if there is nothing selected. 549 */ 550 public Object getSelectedItem() { 551 T adapter = getAdapter(); 552 int selection = getSelectedItemPosition(); 553 if (adapter != null && adapter.getCount() > 0 && selection >= 0) { 554 return adapter.getItem(selection); 555 } else { 556 return null; 557 } 558 } 559 560 /** 561 * @return The number of items owned by the Adapter associated with this 562 * AdapterView. (This is the number of data items, which may be 563 * larger than the number of visible views.) 564 */ 565 @ViewDebug.CapturedViewProperty 566 public int getCount() { 567 return mItemCount; 568 } 569 570 /** 571 * Get the position within the adapter's data set for the view, where view is a an adapter item 572 * or a descendant of an adapter item. 573 * 574 * @param view an adapter item, or a descendant of an adapter item. This must be visible in this 575 * AdapterView at the time of the call. 576 * @return the position within the adapter's data set of the view, or {@link #INVALID_POSITION} 577 * if the view does not correspond to a list item (or it is not currently visible). 578 */ 579 public int getPositionForView(View view) { 580 View listItem = view; 581 try { 582 View v; 583 while (!(v = (View) listItem.getParent()).equals(this)) { 584 listItem = v; 585 } 586 } catch (ClassCastException e) { 587 // We made it up to the window without find this list view 588 return INVALID_POSITION; 589 } 590 591 // Search the children for the list item 592 final int childCount = getChildCount(); 593 for (int i = 0; i < childCount; i++) { 594 if (getChildAt(i).equals(listItem)) { 595 return mFirstPosition + i; 596 } 597 } 598 599 // Child not found! 600 return INVALID_POSITION; 601 } 602 603 /** 604 * Returns the position within the adapter's data set for the first item 605 * displayed on screen. 606 * 607 * @return The position within the adapter's data set 608 */ 609 public int getFirstVisiblePosition() { 610 return mFirstPosition; 611 } 612 613 /** 614 * Returns the position within the adapter's data set for the last item 615 * displayed on screen. 616 * 617 * @return The position within the adapter's data set 618 */ 619 public int getLastVisiblePosition() { 620 return mFirstPosition + getChildCount() - 1; 621 } 622 623 /** 624 * Sets the currently selected item. To support accessibility subclasses that 625 * override this method must invoke the overriden super method first. 626 * 627 * @param position Index (starting at 0) of the data item to be selected. 628 */ 629 public abstract void setSelection(int position); 630 631 /** 632 * Sets the view to show if the adapter is empty 633 */ 634 @android.view.RemotableViewMethod 635 public void setEmptyView(View emptyView) { 636 mEmptyView = emptyView; 637 638 final T adapter = getAdapter(); 639 final boolean empty = ((adapter == null) || adapter.isEmpty()); 640 updateEmptyStatus(empty); 641 } 642 643 /** 644 * When the current adapter is empty, the AdapterView can display a special view 645 * call the empty view. The empty view is used to provide feedback to the user 646 * that no data is available in this AdapterView. 647 * 648 * @return The view to show if the adapter is empty. 649 */ 650 public View getEmptyView() { 651 return mEmptyView; 652 } 653 654 /** 655 * Indicates whether this view is in filter mode. Filter mode can for instance 656 * be enabled by a user when typing on the keyboard. 657 * 658 * @return True if the view is in filter mode, false otherwise. 659 */ 660 boolean isInFilterMode() { 661 return false; 662 } 663 664 @Override 665 public void setFocusable(boolean focusable) { 666 final T adapter = getAdapter(); 667 final boolean empty = adapter == null || adapter.getCount() == 0; 668 669 mDesiredFocusableState = focusable; 670 if (!focusable) { 671 mDesiredFocusableInTouchModeState = false; 672 } 673 674 super.setFocusable(focusable && (!empty || isInFilterMode())); 675 } 676 677 @Override 678 public void setFocusableInTouchMode(boolean focusable) { 679 final T adapter = getAdapter(); 680 final boolean empty = adapter == null || adapter.getCount() == 0; 681 682 mDesiredFocusableInTouchModeState = focusable; 683 if (focusable) { 684 mDesiredFocusableState = true; 685 } 686 687 super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode())); 688 } 689 690 void checkFocus() { 691 final T adapter = getAdapter(); 692 final boolean empty = adapter == null || adapter.getCount() == 0; 693 final boolean focusable = !empty || isInFilterMode(); 694 // The order in which we set focusable in touch mode/focusable may matter 695 // for the client, see View.setFocusableInTouchMode() comments for more 696 // details 697 super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState); 698 super.setFocusable(focusable && mDesiredFocusableState); 699 if (mEmptyView != null) { 700 updateEmptyStatus((adapter == null) || adapter.isEmpty()); 701 } 702 } 703 704 /** 705 * Update the status of the list based on the empty parameter. If empty is true and 706 * we have an empty view, display it. In all the other cases, make sure that the listview 707 * is VISIBLE and that the empty view is GONE (if it's not null). 708 */ 709 private void updateEmptyStatus(boolean empty) { 710 if (isInFilterMode()) { 711 empty = false; 712 } 713 714 if (empty) { 715 if (mEmptyView != null) { 716 mEmptyView.setVisibility(View.VISIBLE); 717 setVisibility(View.GONE); 718 } else { 719 // If the caller just removed our empty view, make sure the list view is visible 720 setVisibility(View.VISIBLE); 721 } 722 723 // We are now GONE, so pending layouts will not be dispatched. 724 // Force one here to make sure that the state of the list matches 725 // the state of the adapter. 726 if (mDataChanged) { 727 this.onLayout(false, mLeft, mTop, mRight, mBottom); 728 } 729 } else { 730 if (mEmptyView != null) mEmptyView.setVisibility(View.GONE); 731 setVisibility(View.VISIBLE); 732 } 733 } 734 735 /** 736 * Gets the data associated with the specified position in the list. 737 * 738 * @param position Which data to get 739 * @return The data associated with the specified position in the list 740 */ 741 public Object getItemAtPosition(int position) { 742 T adapter = getAdapter(); 743 return (adapter == null || position < 0) ? null : adapter.getItem(position); 744 } 745 746 public long getItemIdAtPosition(int position) { 747 T adapter = getAdapter(); 748 return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position); 749 } 750 751 @Override 752 public void setOnClickListener(OnClickListener l) { 753 throw new RuntimeException("Don't call setOnClickListener for an AdapterView. " 754 + "You probably want setOnItemClickListener instead"); 755 } 756 757 /** 758 * Override to prevent freezing of any views created by the adapter. 759 */ 760 @Override 761 protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { 762 dispatchFreezeSelfOnly(container); 763 } 764 765 /** 766 * Override to prevent thawing of any views created by the adapter. 767 */ 768 @Override 769 protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { 770 dispatchThawSelfOnly(container); 771 } 772 773 class AdapterDataSetObserver extends DataSetObserver { 774 775 private Parcelable mInstanceState = null; 776 777 @Override 778 public void onChanged() { 779 mDataChanged = true; 780 mOldItemCount = mItemCount; 781 mItemCount = getAdapter().getCount(); 782 783 // Detect the case where a cursor that was previously invalidated has 784 // been repopulated with new data. 785 if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null 786 && mOldItemCount == 0 && mItemCount > 0) { 787 AdapterView.this.onRestoreInstanceState(mInstanceState); 788 mInstanceState = null; 789 } else { 790 rememberSyncState(); 791 } 792 checkFocus(); 793 requestLayout(); 794 } 795 796 @Override 797 public void onInvalidated() { 798 mDataChanged = true; 799 800 if (AdapterView.this.getAdapter().hasStableIds()) { 801 // Remember the current state for the case where our hosting activity is being 802 // stopped and later restarted 803 mInstanceState = AdapterView.this.onSaveInstanceState(); 804 } 805 806 // Data is invalid so we should reset our state 807 mOldItemCount = mItemCount; 808 mItemCount = 0; 809 mSelectedPosition = INVALID_POSITION; 810 mSelectedRowId = INVALID_ROW_ID; 811 mNextSelectedPosition = INVALID_POSITION; 812 mNextSelectedRowId = INVALID_ROW_ID; 813 mNeedSync = false; 814 815 checkFocus(); 816 requestLayout(); 817 } 818 819 public void clearSavedState() { 820 mInstanceState = null; 821 } 822 } 823 824 @Override 825 protected void onDetachedFromWindow() { 826 super.onDetachedFromWindow(); 827 removeCallbacks(mSelectionNotifier); 828 } 829 830 private class SelectionNotifier implements Runnable { 831 public void run() { 832 if (mDataChanged) { 833 // Data has changed between when this SelectionNotifier 834 // was posted and now. We need to wait until the AdapterView 835 // has been synched to the new data. 836 if (getAdapter() != null) { 837 post(this); 838 } 839 } else { 840 fireOnSelected(); 841 } 842 } 843 } 844 845 void selectionChanged() { 846 if (mOnItemSelectedListener != null) { 847 if (mInLayout || mBlockLayoutRequests) { 848 // If we are in a layout traversal, defer notification 849 // by posting. This ensures that the view tree is 850 // in a consistent state and is able to accomodate 851 // new layout or invalidate requests. 852 if (mSelectionNotifier == null) { 853 mSelectionNotifier = new SelectionNotifier(); 854 } 855 post(mSelectionNotifier); 856 } else { 857 fireOnSelected(); 858 } 859 } 860 861 // we fire selection events here not in View 862 if (mSelectedPosition != ListView.INVALID_POSITION && isShown() && !isInTouchMode()) { 863 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 864 } 865 } 866 867 private void fireOnSelected() { 868 if (mOnItemSelectedListener == null) 869 return; 870 871 int selection = this.getSelectedItemPosition(); 872 if (selection >= 0) { 873 View v = getSelectedView(); 874 mOnItemSelectedListener.onItemSelected(this, v, selection, 875 getAdapter().getItemId(selection)); 876 } else { 877 mOnItemSelectedListener.onNothingSelected(this); 878 } 879 } 880 881 @Override 882 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 883 // This is an exceptional case which occurs when a window gets the 884 // focus and sends a focus event via its focused child to announce 885 // current focus/selection. AdapterView fires selection but not focus 886 // events so we change the event type here. 887 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED) { 888 event.setEventType(AccessibilityEvent.TYPE_VIEW_SELECTED); 889 } 890 891 View selectedView = getSelectedView(); 892 if (selectedView != null && selectedView.getVisibility() == VISIBLE) { 893 // We first get a chance to populate the event. 894 onPopulateAccessibilityEvent(event); 895 } 896 return false; 897 } 898 899 @Override 900 public void onPopulateAccessibilityEvent(AccessibilityEvent event) { 901 super.onPopulateAccessibilityEvent(event); 902 // We send selection events only from AdapterView to avoid 903 // generation of such event for each child. 904 getSelectedView().dispatchPopulateAccessibilityEvent(event); 905 } 906 907 @Override 908 public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { 909 if (super.onRequestSendAccessibilityEvent(child, event)) { 910 // Add a record for ourselves as well. 911 AccessibilityEvent record = AccessibilityEvent.obtain(); 912 onInitializeAccessibilityEvent(record); 913 // Populate with the text of the requesting child. 914 child.dispatchPopulateAccessibilityEvent(record); 915 event.appendRecord(record); 916 return true; 917 } 918 return false; 919 } 920 921 @Override 922 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 923 super.onInitializeAccessibilityEvent(event); 924 925 View selectedView = getSelectedView(); 926 if (selectedView != null) { 927 event.setEnabled(selectedView.isEnabled()); 928 } 929 event.setItemCount(getCount()); 930 event.setCurrentItemIndex(getSelectedItemPosition()); 931 if (getChildCount() > 0) { 932 event.setFromIndex(getFirstVisiblePosition()); 933 event.setToIndex(getLastVisiblePosition()); 934 } 935 } 936 937 @Override 938 protected boolean canAnimate() { 939 return super.canAnimate() && mItemCount > 0; 940 } 941 942 void handleDataChanged() { 943 final int count = mItemCount; 944 boolean found = false; 945 946 if (count > 0) { 947 948 int newPos; 949 950 // Find the row we are supposed to sync to 951 if (mNeedSync) { 952 // Update this first, since setNextSelectedPositionInt inspects 953 // it 954 mNeedSync = false; 955 956 // See if we can find a position in the new data with the same 957 // id as the old selection 958 newPos = findSyncPosition(); 959 if (newPos >= 0) { 960 // Verify that new selection is selectable 961 int selectablePos = lookForSelectablePosition(newPos, true); 962 if (selectablePos == newPos) { 963 // Same row id is selected 964 setNextSelectedPositionInt(newPos); 965 found = true; 966 } 967 } 968 } 969 if (!found) { 970 // Try to use the same position if we can't find matching data 971 newPos = getSelectedItemPosition(); 972 973 // Pin position to the available range 974 if (newPos >= count) { 975 newPos = count - 1; 976 } 977 if (newPos < 0) { 978 newPos = 0; 979 } 980 981 // Make sure we select something selectable -- first look down 982 int selectablePos = lookForSelectablePosition(newPos, true); 983 if (selectablePos < 0) { 984 // Looking down didn't work -- try looking up 985 selectablePos = lookForSelectablePosition(newPos, false); 986 } 987 if (selectablePos >= 0) { 988 setNextSelectedPositionInt(selectablePos); 989 checkSelectionChanged(); 990 found = true; 991 } 992 } 993 } 994 if (!found) { 995 // Nothing is selected 996 mSelectedPosition = INVALID_POSITION; 997 mSelectedRowId = INVALID_ROW_ID; 998 mNextSelectedPosition = INVALID_POSITION; 999 mNextSelectedRowId = INVALID_ROW_ID; 1000 mNeedSync = false; 1001 checkSelectionChanged(); 1002 } 1003 } 1004 1005 void checkSelectionChanged() { 1006 if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) { 1007 selectionChanged(); 1008 mOldSelectedPosition = mSelectedPosition; 1009 mOldSelectedRowId = mSelectedRowId; 1010 } 1011 } 1012 1013 /** 1014 * Searches the adapter for a position matching mSyncRowId. The search starts at mSyncPosition 1015 * and then alternates between moving up and moving down until 1) we find the right position, or 1016 * 2) we run out of time, or 3) we have looked at every position 1017 * 1018 * @return Position of the row that matches mSyncRowId, or {@link #INVALID_POSITION} if it can't 1019 * be found 1020 */ 1021 int findSyncPosition() { 1022 int count = mItemCount; 1023 1024 if (count == 0) { 1025 return INVALID_POSITION; 1026 } 1027 1028 long idToMatch = mSyncRowId; 1029 int seed = mSyncPosition; 1030 1031 // If there isn't a selection don't hunt for it 1032 if (idToMatch == INVALID_ROW_ID) { 1033 return INVALID_POSITION; 1034 } 1035 1036 // Pin seed to reasonable values 1037 seed = Math.max(0, seed); 1038 seed = Math.min(count - 1, seed); 1039 1040 long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS; 1041 1042 long rowId; 1043 1044 // first position scanned so far 1045 int first = seed; 1046 1047 // last position scanned so far 1048 int last = seed; 1049 1050 // True if we should move down on the next iteration 1051 boolean next = false; 1052 1053 // True when we have looked at the first item in the data 1054 boolean hitFirst; 1055 1056 // True when we have looked at the last item in the data 1057 boolean hitLast; 1058 1059 // Get the item ID locally (instead of getItemIdAtPosition), so 1060 // we need the adapter 1061 T adapter = getAdapter(); 1062 if (adapter == null) { 1063 return INVALID_POSITION; 1064 } 1065 1066 while (SystemClock.uptimeMillis() <= endTime) { 1067 rowId = adapter.getItemId(seed); 1068 if (rowId == idToMatch) { 1069 // Found it! 1070 return seed; 1071 } 1072 1073 hitLast = last == count - 1; 1074 hitFirst = first == 0; 1075 1076 if (hitLast && hitFirst) { 1077 // Looked at everything 1078 break; 1079 } 1080 1081 if (hitFirst || (next && !hitLast)) { 1082 // Either we hit the top, or we are trying to move down 1083 last++; 1084 seed = last; 1085 // Try going up next time 1086 next = false; 1087 } else if (hitLast || (!next && !hitFirst)) { 1088 // Either we hit the bottom, or we are trying to move up 1089 first--; 1090 seed = first; 1091 // Try going down next time 1092 next = true; 1093 } 1094 1095 } 1096 1097 return INVALID_POSITION; 1098 } 1099 1100 /** 1101 * Find a position that can be selected (i.e., is not a separator). 1102 * 1103 * @param position The starting position to look at. 1104 * @param lookDown Whether to look down for other positions. 1105 * @return The next selectable position starting at position and then searching either up or 1106 * down. Returns {@link #INVALID_POSITION} if nothing can be found. 1107 */ 1108 int lookForSelectablePosition(int position, boolean lookDown) { 1109 return position; 1110 } 1111 1112 /** 1113 * Utility to keep mSelectedPosition and mSelectedRowId in sync 1114 * @param position Our current position 1115 */ 1116 void setSelectedPositionInt(int position) { 1117 mSelectedPosition = position; 1118 mSelectedRowId = getItemIdAtPosition(position); 1119 } 1120 1121 /** 1122 * Utility to keep mNextSelectedPosition and mNextSelectedRowId in sync 1123 * @param position Intended value for mSelectedPosition the next time we go 1124 * through layout 1125 */ 1126 void setNextSelectedPositionInt(int position) { 1127 mNextSelectedPosition = position; 1128 mNextSelectedRowId = getItemIdAtPosition(position); 1129 // If we are trying to sync to the selection, update that too 1130 if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) { 1131 mSyncPosition = position; 1132 mSyncRowId = mNextSelectedRowId; 1133 } 1134 } 1135 1136 /** 1137 * Remember enough information to restore the screen state when the data has 1138 * changed. 1139 * 1140 */ 1141 void rememberSyncState() { 1142 if (getChildCount() > 0) { 1143 mNeedSync = true; 1144 mSyncHeight = mLayoutHeight; 1145 if (mSelectedPosition >= 0) { 1146 // Sync the selection state 1147 View v = getChildAt(mSelectedPosition - mFirstPosition); 1148 mSyncRowId = mNextSelectedRowId; 1149 mSyncPosition = mNextSelectedPosition; 1150 if (v != null) { 1151 mSpecificTop = v.getTop(); 1152 } 1153 mSyncMode = SYNC_SELECTED_POSITION; 1154 } else { 1155 // Sync the based on the offset of the first view 1156 View v = getChildAt(0); 1157 T adapter = getAdapter(); 1158 if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) { 1159 mSyncRowId = adapter.getItemId(mFirstPosition); 1160 } else { 1161 mSyncRowId = NO_ID; 1162 } 1163 mSyncPosition = mFirstPosition; 1164 if (v != null) { 1165 mSpecificTop = v.getTop(); 1166 } 1167 mSyncMode = SYNC_FIRST_POSITION; 1168 } 1169 } 1170 } 1171} 1172