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