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