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