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