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